@vio-live/web-sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3859 @@
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
+ // No separate staging GraphQL deployment yet — shares dev's.
21
+ testing: {
22
+ apiBase: "https://api-staging.vio.live",
23
+ graphQLBase: "https://graph-ql-dev.vio.live"
24
+ },
25
+ production: {
26
+ apiBase: "https://api.vio.live",
27
+ graphQLBase: "https://graph-ql.vio.live"
28
+ }
29
+ };
30
+ var ConfigurationSingleton = class {
31
+ constructor() {
32
+ this.resolved = null;
33
+ this.readyWaiters = [];
34
+ }
35
+ init(input) {
36
+ if (!input.apiKey || typeof input.apiKey !== "string") {
37
+ throw new Error("[Vio] apiKey is required in Vio.init({ apiKey })");
38
+ }
39
+ const env = input.environment ?? "development";
40
+ const defaults = DEFAULT_URLS[env];
41
+ this.resolved = {
42
+ apiKey: input.apiKey,
43
+ environment: env,
44
+ apiBase: input.apiBase ?? defaults.apiBase,
45
+ graphQLBase: input.graphQLBase ?? defaults.graphQLBase,
46
+ stripePublishableKey: input.stripePublishableKey,
47
+ klarnaClientId: input.klarnaClientId,
48
+ // Sandbox by default — live Klarna must be opted into explicitly.
49
+ klarnaEnvironment: input.klarnaEnvironment ?? "playground"
50
+ };
51
+ if (typeof console !== "undefined") {
52
+ const masked = input.apiKey.length > 4 ? "*".repeat(Math.max(0, input.apiKey.length - 4)) + input.apiKey.slice(-4) : "****";
53
+ console.info("[Vio] Initialized", {
54
+ environment: env,
55
+ apiBase: this.resolved.apiBase,
56
+ apiKey: masked,
57
+ stripeConfigured: !!input.stripePublishableKey,
58
+ klarnaConfigured: !!input.klarnaClientId,
59
+ klarnaEnvironment: this.resolved.klarnaEnvironment
60
+ });
61
+ }
62
+ const waiters = this.readyWaiters;
63
+ this.readyWaiters = [];
64
+ for (const waiter of waiters) waiter();
65
+ }
66
+ get() {
67
+ if (!this.resolved) {
68
+ throw new Error(
69
+ "[Vio] Not initialized. Call Vio.init({ apiKey }) before using the SDK."
70
+ );
71
+ }
72
+ return this.resolved;
73
+ }
74
+ get isInitialized() {
75
+ return this.resolved !== null;
76
+ }
77
+ /**
78
+ * Resolves once `Vio.init()` has run, or after `timeoutMs` — whichever
79
+ * comes first. Hosts that mount several independent components (e.g. Vev
80
+ * blocks, which mount in no guaranteed order) may have one fire a GraphQL
81
+ * call before another has called `Vio.init()`. Awaiting this briefly lets
82
+ * that race resolve instead of silently sending an unauthenticated
83
+ * request. Resolves `true` if init happened in time, `false` on timeout —
84
+ * callers should still degrade gracefully either way.
85
+ */
86
+ whenReady(timeoutMs = 2e3) {
87
+ if (this.resolved) return Promise.resolve(true);
88
+ return new Promise((resolve) => {
89
+ const timer = setTimeout(() => resolve(false), timeoutMs);
90
+ this.readyWaiters.push(() => {
91
+ clearTimeout(timer);
92
+ resolve(true);
93
+ });
94
+ });
95
+ }
96
+ /** For tests only. */
97
+ reset() {
98
+ this.resolved = null;
99
+ this.readyWaiters = [];
100
+ }
101
+ };
102
+ var Configuration = /* @__PURE__ */ new ConfigurationSingleton();
103
+
104
+ // src/core/api/vio.ts
105
+ var VioApi = class {
106
+ constructor(options) {
107
+ if (!options.apiBase) throw new Error("[VioApi] apiBase is required");
108
+ if (!options.apiKey) throw new Error("[VioApi] apiKey is required");
109
+ this.apiBase = options.apiBase.replace(/\/+$/, "");
110
+ this.apiKey = options.apiKey;
111
+ }
112
+ /**
113
+ * Bootstrap — fetches client app config (sponsors with commerce keys,
114
+ * default market, campaign id). Called once at SDK init.
115
+ */
116
+ async bootstrap() {
117
+ return this.get("/v2/mobile/config");
118
+ }
119
+ /**
120
+ * Get placement components for a campaign. Used by the carousel auto-fetch
121
+ * mode (`<vio-product-carousel campaign-id="36" location="news-favorites">`).
122
+ */
123
+ async getComponents(campaignId) {
124
+ const path = `/v2/mobile/campaigns/${encodeURIComponent(String(campaignId))}/components`;
125
+ const res = await this.get(path);
126
+ return Array.isArray(res) ? res : res?.components ?? [];
127
+ }
128
+ // MARK: - HTTP plumbing
129
+ async get(path) {
130
+ const url = `${this.apiBase}${path}`;
131
+ const res = await fetch(url, {
132
+ method: "GET",
133
+ headers: {
134
+ "X-API-Key": this.apiKey,
135
+ Accept: "application/json"
136
+ }
137
+ });
138
+ if (!res.ok) {
139
+ throw new VioApiError(res.status, await safeText(res), `GET ${path}`);
140
+ }
141
+ return await res.json();
142
+ }
143
+ };
144
+ var VioApiError = class extends Error {
145
+ constructor(status, body, endpoint) {
146
+ super(`[VioApi] ${endpoint} \u2192 HTTP ${status}: ${body.slice(0, 200)}`);
147
+ this.status = status;
148
+ this.body = body;
149
+ this.endpoint = endpoint;
150
+ this.name = "VioApiError";
151
+ }
152
+ };
153
+ async function safeText(res) {
154
+ try {
155
+ return await res.text();
156
+ } catch {
157
+ return "";
158
+ }
159
+ }
160
+ function createVioApi(options) {
161
+ return new VioApi(options);
162
+ }
163
+
164
+ // src/core/types.ts
165
+ function displayPrice(product) {
166
+ const p = product.price;
167
+ const amount = p.amount_incl_taxes ?? p.amount;
168
+ return formatPrice(amount, p.currency_code);
169
+ }
170
+ function getGlobalCountryCode() {
171
+ const g = globalThis;
172
+ if (g.__VIO_COUNTRY__) return g.__VIO_COUNTRY__;
173
+ if (typeof localStorage !== "undefined") {
174
+ try {
175
+ const v = localStorage.getItem("vio.country");
176
+ if (v) return v;
177
+ } catch {
178
+ }
179
+ }
180
+ return "NO";
181
+ }
182
+ function getGlobalLocale() {
183
+ const g = globalThis;
184
+ if (g.__VIO_LOCALE__) return g.__VIO_LOCALE__;
185
+ if (typeof localStorage !== "undefined") {
186
+ try {
187
+ const v = localStorage.getItem("vio.locale");
188
+ if (v) return v;
189
+ } catch {
190
+ }
191
+ }
192
+ return "nb-NO";
193
+ }
194
+ function getGlobalSymbol(currency) {
195
+ const activeCurrency = currency || getGlobalCurrency();
196
+ const g = globalThis;
197
+ if (!currency && g.__VIO_SYMBOL__) return g.__VIO_SYMBOL__;
198
+ if (!currency && typeof localStorage !== "undefined") {
199
+ try {
200
+ const v = localStorage.getItem("vio.symbol");
201
+ if (v) return v;
202
+ } catch {
203
+ }
204
+ }
205
+ const curr = String(activeCurrency).toUpperCase();
206
+ if (curr === "NOK" || curr === "DKK") return "Kr";
207
+ if (curr === "SEK") return "SEK";
208
+ if (curr === "EUR") return "\u20AC";
209
+ if (curr === "USD" || curr === "CAD") return "$";
210
+ if (curr === "GBP") return "\xA3";
211
+ if (curr === "CHF") return "CHF";
212
+ return activeCurrency;
213
+ }
214
+ function getGlobalCurrency() {
215
+ const g = globalThis;
216
+ if (g.__VIO_CURRENCY__) return g.__VIO_CURRENCY__;
217
+ if (typeof localStorage !== "undefined") {
218
+ try {
219
+ const v = localStorage.getItem("vio.currency");
220
+ if (v) return v;
221
+ } catch {
222
+ }
223
+ }
224
+ return "NOK";
225
+ }
226
+ var validatedLocales = /* @__PURE__ */ new Map();
227
+ function safeLocale(locale) {
228
+ const cached = validatedLocales.get(locale);
229
+ if (cached) return cached;
230
+ let resolved = "nb-NO";
231
+ try {
232
+ if (Intl.NumberFormat.supportedLocalesOf([locale]).length > 0) resolved = locale;
233
+ } catch {
234
+ }
235
+ validatedLocales.set(locale, resolved);
236
+ return resolved;
237
+ }
238
+ function formatPrice(amount, currency) {
239
+ const activeCurrency = currency || getGlobalCurrency();
240
+ const activeLocale = safeLocale(getGlobalLocale());
241
+ const activeSymbol = getGlobalSymbol(activeCurrency);
242
+ const formatted = amount.toLocaleString(activeLocale, { minimumFractionDigits: 0, maximumFractionDigits: 2 }).replace(/ /g, " ");
243
+ return `${formatted} ${activeSymbol}`;
244
+ }
245
+ function primaryImageUrl(product) {
246
+ const p = product;
247
+ if (!p || typeof p !== "object") return void 0;
248
+ if (Array.isArray(p.variants) && p.variants.length > 0) {
249
+ const vImages = p.variants[0]?.images;
250
+ if (Array.isArray(vImages) && vImages.length > 0) {
251
+ const vUrl = typeof vImages[0] === "string" ? vImages[0] : vImages[0]?.url;
252
+ if (vUrl) return vUrl;
253
+ }
254
+ }
255
+ if (Array.isArray(p.images) && p.images.length > 0) {
256
+ const img0 = p.images[0];
257
+ const url = typeof img0 === "string" ? img0 : img0?.url;
258
+ if (url) return url;
259
+ }
260
+ if (typeof p.image === "string" && p.image) return p.image;
261
+ if (p.image && typeof p.image === "object" && p.image.url) return p.image.url;
262
+ if (typeof p.imageUrl === "string" && p.imageUrl) return p.imageUrl;
263
+ if (typeof p.image_url === "string" && p.image_url) return p.image_url;
264
+ if (typeof p.primaryImage === "string" && p.primaryImage) return p.primaryImage;
265
+ if (p.primaryImage && typeof p.primaryImage === "object" && p.primaryImage.url) {
266
+ return p.primaryImage.url;
267
+ }
268
+ return void 0;
269
+ }
270
+ var GET_PRODUCTS_BY_IDS = (
271
+ /* GraphQL */
272
+ `
273
+ query GetProductsByIds(
274
+ $currency: String
275
+ $imageSize: ImageSize
276
+ $productIds: [Int!]
277
+ $useCache: Boolean!
278
+ $shippingCountryCode: String
279
+ ) {
280
+ Channel {
281
+ GetProductsByIds(
282
+ currency: $currency
283
+ image_size: $imageSize
284
+ product_ids: $productIds
285
+ useCache: $useCache
286
+ shipping_country_code: $shippingCountryCode
287
+ ) {
288
+ id
289
+ brand
290
+ title
291
+ description
292
+ tags
293
+ sku
294
+ quantity
295
+ price {
296
+ amount
297
+ currency_code
298
+ compare_at
299
+ amount_incl_taxes
300
+ compare_at_incl_taxes
301
+ tax_amount
302
+ tax_rate
303
+ }
304
+ variants {
305
+ id
306
+ barcode
307
+ quantity
308
+ sku
309
+ title
310
+ price {
311
+ amount
312
+ currency_code
313
+ compare_at
314
+ amount_incl_taxes
315
+ compare_at_incl_taxes
316
+ tax_amount
317
+ tax_rate
318
+ }
319
+ images {
320
+ id
321
+ url
322
+ width
323
+ height
324
+ order
325
+ }
326
+ }
327
+ barcode
328
+ options {
329
+ id
330
+ name
331
+ order
332
+ values
333
+ }
334
+ categories {
335
+ id
336
+ name
337
+ }
338
+ images {
339
+ id
340
+ url
341
+ width
342
+ height
343
+ order
344
+ }
345
+ }
346
+ }
347
+ }
348
+ `
349
+ );
350
+ var ChannelProduct = class {
351
+ constructor(client) {
352
+ this.client = client;
353
+ }
354
+ /**
355
+ * Fetch a batch of products by their numeric ids.
356
+ * Returns an empty array if no ids are provided.
357
+ */
358
+ async getByIds(options) {
359
+ if (!options.product_ids || options.product_ids.length === 0) return [];
360
+ const variables = {
361
+ productIds: options.product_ids,
362
+ // Page-level currency/country defaults (multi-market support).
363
+ currency: options.currency ?? getGlobalCurrency(),
364
+ imageSize: options.image_size ?? "large",
365
+ useCache: options.useCache ?? true,
366
+ shippingCountryCode: options.shipping_country_code ?? getGlobalCountryCode()
367
+ };
368
+ const data = await this.client.request(GET_PRODUCTS_BY_IDS, variables);
369
+ return data.Channel?.GetProductsByIds ?? [];
370
+ }
371
+ };
372
+ var Channel = class {
373
+ constructor(client) {
374
+ this.product = new ChannelProduct(client);
375
+ }
376
+ };
377
+ var CommerceClient = class {
378
+ constructor(options) {
379
+ if (!options.endpoint) throw new Error("[VioCommerce] endpoint is required");
380
+ if (!options.apiKey) throw new Error("[VioCommerce] apiKey is required");
381
+ this.raw = new GraphQLClient(options.endpoint, {
382
+ headers: { Authorization: options.apiKey }
383
+ });
384
+ this.channel = new Channel(this.raw);
385
+ }
386
+ };
387
+ function createCommerceClient(options) {
388
+ return new CommerceClient(options);
389
+ }
390
+
391
+ // src/core/api/cart-queries.ts
392
+ var CREATE_CART_MUTATION = `
393
+ mutation CreateCart($customerSessionId: String!, $currency: String!, $shippingCountry: String) {
394
+ Cart {
395
+ CreateCart(customer_session_id: $customerSessionId, currency: $currency, shipping_country: $shippingCountry) {
396
+ cart_id
397
+ customer_session_id
398
+ shipping_country
399
+ line_items {
400
+ id
401
+ supplier
402
+ image {
403
+ id
404
+ url
405
+ width
406
+ height
407
+ }
408
+ sku
409
+ barcode
410
+ brand
411
+ product_id
412
+ title
413
+ variant_id
414
+ variant_title
415
+ variant {
416
+ option
417
+ value
418
+ }
419
+ quantity
420
+ price {
421
+ amount
422
+ currency_code
423
+ discount
424
+ compare_at
425
+ compare_at_incl_taxes
426
+ amount_incl_taxes
427
+ tax_amount
428
+ tax_rate
429
+ }
430
+ shipping {
431
+ id
432
+ name
433
+ description
434
+ price {
435
+ amount
436
+ currency_code
437
+ amount_incl_taxes
438
+ tax_amount
439
+ tax_rate
440
+ }
441
+ }
442
+ available_shippings {
443
+ id
444
+ name
445
+ description
446
+ country_code
447
+ price {
448
+ amount
449
+ currency_code
450
+ amount_incl_taxes
451
+ tax_amount
452
+ tax_rate
453
+ }
454
+ }
455
+ }
456
+ subtotal
457
+ shipping
458
+ currency
459
+ available_shipping_countries
460
+ }
461
+ }
462
+ }
463
+ `;
464
+ var ADD_ITEM_MUTATION = `
465
+ mutation AddItem($cartId: String!, $lineItems: [LineItemInput!]!) {
466
+ Cart {
467
+ AddItem(cart_id: $cartId, line_items: $lineItems) {
468
+ cart_id
469
+ customer_session_id
470
+ shipping_country
471
+ line_items {
472
+ id
473
+ supplier
474
+ image {
475
+ id
476
+ url
477
+ width
478
+ height
479
+ }
480
+ sku
481
+ barcode
482
+ brand
483
+ product_id
484
+ title
485
+ variant_id
486
+ variant_title
487
+ variant {
488
+ option
489
+ value
490
+ }
491
+ quantity
492
+ price {
493
+ amount
494
+ currency_code
495
+ discount
496
+ compare_at
497
+ compare_at_incl_taxes
498
+ amount_incl_taxes
499
+ tax_amount
500
+ tax_rate
501
+ }
502
+ shipping {
503
+ id
504
+ name
505
+ description
506
+ price {
507
+ amount
508
+ currency_code
509
+ amount_incl_taxes
510
+ tax_amount
511
+ tax_rate
512
+ }
513
+ }
514
+ available_shippings {
515
+ id
516
+ name
517
+ description
518
+ country_code
519
+ price {
520
+ amount
521
+ currency_code
522
+ amount_incl_taxes
523
+ tax_amount
524
+ tax_rate
525
+ }
526
+ }
527
+ }
528
+ subtotal
529
+ shipping
530
+ currency
531
+ available_shipping_countries
532
+ }
533
+ }
534
+ }
535
+ `;
536
+ var DELETE_ITEM_MUTATION = `
537
+ mutation DeleteItem($cartId: String!, $cartItemId: String!) {
538
+ Cart {
539
+ DeleteItem(cart_id: $cartId, cart_item_id: $cartItemId) {
540
+ cart_id
541
+ customer_session_id
542
+ shipping_country
543
+ line_items {
544
+ id
545
+ supplier
546
+ image {
547
+ id
548
+ url
549
+ width
550
+ height
551
+ }
552
+ sku
553
+ barcode
554
+ brand
555
+ product_id
556
+ title
557
+ variant_id
558
+ variant_title
559
+ variant {
560
+ option
561
+ value
562
+ }
563
+ quantity
564
+ price {
565
+ amount
566
+ currency_code
567
+ discount
568
+ compare_at
569
+ compare_at_incl_taxes
570
+ amount_incl_taxes
571
+ tax_amount
572
+ tax_rate
573
+ }
574
+ shipping {
575
+ id
576
+ name
577
+ description
578
+ price {
579
+ amount
580
+ currency_code
581
+ amount_incl_taxes
582
+ tax_amount
583
+ tax_rate
584
+ }
585
+ }
586
+ available_shippings {
587
+ id
588
+ name
589
+ description
590
+ country_code
591
+ price {
592
+ amount
593
+ currency_code
594
+ amount_incl_taxes
595
+ tax_amount
596
+ tax_rate
597
+ }
598
+ }
599
+ }
600
+ subtotal
601
+ shipping
602
+ currency
603
+ available_shipping_countries
604
+ }
605
+ }
606
+ }
607
+ `;
608
+ var UPDATE_ITEM_MUTATION = `
609
+ mutation UpdateItem($cartId: String!, $cartItemId: String!, $shippingId: String, $qty: Int) {
610
+ Cart {
611
+ UpdateItem(cart_id: $cartId, cart_item_id: $cartItemId, shipping_id: $shippingId, qty: $qty) {
612
+ cart_id
613
+ customer_session_id
614
+ shipping_country
615
+ line_items {
616
+ id
617
+ supplier
618
+ image {
619
+ id
620
+ url
621
+ width
622
+ height
623
+ }
624
+ sku
625
+ barcode
626
+ brand
627
+ product_id
628
+ title
629
+ variant_id
630
+ variant_title
631
+ variant {
632
+ option
633
+ value
634
+ }
635
+ quantity
636
+ price {
637
+ amount
638
+ currency_code
639
+ discount
640
+ compare_at
641
+ compare_at_incl_taxes
642
+ amount_incl_taxes
643
+ tax_amount
644
+ tax_rate
645
+ }
646
+ shipping {
647
+ id
648
+ name
649
+ description
650
+ price {
651
+ amount
652
+ currency_code
653
+ amount_incl_taxes
654
+ tax_amount
655
+ tax_rate
656
+ }
657
+ }
658
+ available_shippings {
659
+ id
660
+ name
661
+ description
662
+ country_code
663
+ price {
664
+ amount
665
+ currency_code
666
+ amount_incl_taxes
667
+ tax_amount
668
+ tax_rate
669
+ }
670
+ }
671
+ }
672
+ subtotal
673
+ shipping
674
+ currency
675
+ available_shipping_countries
676
+ }
677
+ }
678
+ }
679
+ `;
680
+ var GET_CART_QUERY = `
681
+ query GetCart($cartId: String!) {
682
+ Cart {
683
+ GetCart(cart_id: $cartId) {
684
+ cart_id
685
+ customer_session_id
686
+ shipping_country
687
+ line_items {
688
+ id
689
+ supplier
690
+ image {
691
+ id
692
+ url
693
+ width
694
+ height
695
+ }
696
+ sku
697
+ barcode
698
+ brand
699
+ product_id
700
+ title
701
+ variant_id
702
+ variant_title
703
+ variant {
704
+ option
705
+ value
706
+ }
707
+ quantity
708
+ price {
709
+ amount
710
+ currency_code
711
+ discount
712
+ compare_at
713
+ compare_at_incl_taxes
714
+ amount_incl_taxes
715
+ tax_amount
716
+ tax_rate
717
+ }
718
+ shipping {
719
+ id
720
+ name
721
+ description
722
+ price {
723
+ amount
724
+ currency_code
725
+ amount_incl_taxes
726
+ tax_amount
727
+ tax_rate
728
+ }
729
+ }
730
+ available_shippings {
731
+ id
732
+ name
733
+ description
734
+ country_code
735
+ price {
736
+ amount
737
+ currency_code
738
+ amount_incl_taxes
739
+ tax_amount
740
+ tax_rate
741
+ }
742
+ }
743
+ }
744
+ subtotal
745
+ shipping
746
+ currency
747
+ available_shipping_countries
748
+ }
749
+ }
750
+ }
751
+ `;
752
+ var GET_LINE_ITEMS_BY_SUPPLIER_QUERY = `
753
+ query GetLineItemsBySupplier($cartId: String!) {
754
+ Cart {
755
+ GetLineItemsBySupplier(cart_id: $cartId) {
756
+ supplier {
757
+ id
758
+ name
759
+ }
760
+ available_shippings {
761
+ id
762
+ name
763
+ description
764
+ country_code
765
+ price {
766
+ amount
767
+ currency_code
768
+ amount_incl_taxes
769
+ tax_amount
770
+ tax_rate
771
+ }
772
+ }
773
+ line_items {
774
+ id
775
+ supplier
776
+ image {
777
+ id
778
+ url
779
+ width
780
+ height
781
+ }
782
+ sku
783
+ barcode
784
+ brand
785
+ product_id
786
+ title
787
+ variant_id
788
+ variant_title
789
+ variant {
790
+ option
791
+ value
792
+ }
793
+ quantity
794
+ price {
795
+ amount
796
+ currency_code
797
+ discount
798
+ compare_at
799
+ compare_at_incl_taxes
800
+ amount_incl_taxes
801
+ tax_amount
802
+ tax_rate
803
+ }
804
+ shipping {
805
+ id
806
+ name
807
+ description
808
+ price {
809
+ amount
810
+ currency_code
811
+ amount_incl_taxes
812
+ tax_amount
813
+ tax_rate
814
+ }
815
+ }
816
+ }
817
+ }
818
+ }
819
+ }
820
+ `;
821
+ var UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION = `
822
+ mutation UpdateShippingsBySupplier($cartId: String!, $data: [DataUpdateShippingsBySupplier!]!) {
823
+ Cart {
824
+ UpdateShippingsBySupplier(cart_id: $cartId, data: $data) {
825
+ cart_id
826
+ customer_session_id
827
+ shipping_country
828
+ line_items {
829
+ id
830
+ supplier
831
+ image {
832
+ id
833
+ url
834
+ width
835
+ height
836
+ }
837
+ sku
838
+ barcode
839
+ brand
840
+ product_id
841
+ title
842
+ variant_id
843
+ variant_title
844
+ variant {
845
+ option
846
+ value
847
+ }
848
+ quantity
849
+ price {
850
+ amount
851
+ currency_code
852
+ discount
853
+ compare_at
854
+ compare_at_incl_taxes
855
+ amount_incl_taxes
856
+ tax_amount
857
+ tax_rate
858
+ }
859
+ shipping {
860
+ id
861
+ name
862
+ description
863
+ price {
864
+ amount
865
+ currency_code
866
+ amount_incl_taxes
867
+ tax_amount
868
+ tax_rate
869
+ }
870
+ }
871
+ available_shippings {
872
+ id
873
+ name
874
+ description
875
+ country_code
876
+ price {
877
+ amount
878
+ currency_code
879
+ amount_incl_taxes
880
+ tax_amount
881
+ tax_rate
882
+ }
883
+ }
884
+ }
885
+ subtotal
886
+ shipping
887
+ currency
888
+ available_shipping_countries
889
+ }
890
+ }
891
+ }
892
+ `;
893
+ var CREATE_CHECKOUT_MUTATION = `
894
+ mutation CreateCheckout($cartId: String!) {
895
+ Checkout {
896
+ CreateCheckout(cart_id: $cartId) {
897
+ buyer_accepts_purchase_conditions
898
+ buyer_accepts_terms_conditions
899
+ created_at
900
+ updated_at
901
+ id
902
+ deleted_at
903
+ success_url
904
+ cancel_url
905
+ payment_method
906
+ email
907
+ status
908
+ checkout_url
909
+ origin_payment_id
910
+ billing_address {
911
+ id
912
+ first_name
913
+ last_name
914
+ phone_code
915
+ phone
916
+ company
917
+ address1
918
+ address2
919
+ city
920
+ province
921
+ province_code
922
+ country
923
+ country_code
924
+ zip
925
+ }
926
+ shipping_address {
927
+ id
928
+ first_name
929
+ last_name
930
+ phone_code
931
+ phone
932
+ company
933
+ address1
934
+ address2
935
+ city
936
+ province
937
+ province_code
938
+ country
939
+ country_code
940
+ zip
941
+ }
942
+ available_payment_methods {
943
+ name
944
+ }
945
+ discount_code
946
+ cart {
947
+ cart_id
948
+ customer_session_id
949
+ shipping_country
950
+ line_items {
951
+ id
952
+ supplier
953
+ image {
954
+ id
955
+ url
956
+ width
957
+ height
958
+ }
959
+ sku
960
+ barcode
961
+ brand
962
+ product_id
963
+ title
964
+ variant_id
965
+ variant_title
966
+ variant {
967
+ option
968
+ value
969
+ }
970
+ quantity
971
+ price {
972
+ amount
973
+ currency_code
974
+ discount
975
+ compare_at
976
+ compare_at_incl_taxes
977
+ amount_incl_taxes
978
+ tax_amount
979
+ tax_rate
980
+ }
981
+ shipping {
982
+ id
983
+ name
984
+ description
985
+ price {
986
+ amount
987
+ currency_code
988
+ amount_incl_taxes
989
+ tax_amount
990
+ tax_rate
991
+ }
992
+ }
993
+ available_shippings {
994
+ id
995
+ name
996
+ description
997
+ country_code
998
+ price {
999
+ amount
1000
+ currency_code
1001
+ amount_incl_taxes
1002
+ tax_amount
1003
+ tax_rate
1004
+ }
1005
+ }
1006
+ }
1007
+ subtotal
1008
+ shipping
1009
+ currency
1010
+ available_shipping_countries
1011
+ }
1012
+ totals {
1013
+ currency_code
1014
+ subtotal
1015
+ shipping
1016
+ total
1017
+ taxes
1018
+ tax_rate
1019
+ discounts
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ `;
1025
+ var UPDATE_CHECKOUT_MUTATION = `
1026
+ mutation UpdateCheckout($checkoutId: String!, $status: String, $successUrl: String, $email: String, $cancelUrl: String, $paymentMethod: String, $shippingAddress: AddressArgs, $billingAddress: AddressArgs, $buyerAcceptsPurchaseConditions: Boolean, $buyerAcceptsTermsConditions: Boolean) {
1027
+ Checkout {
1028
+ UpdateCheckout(checkout_id: $checkoutId, status: $status, success_url: $successUrl, email: $email, cancel_url: $cancelUrl, payment_method: $paymentMethod, shipping_address: $shippingAddress, billing_address: $billingAddress, buyer_accepts_purchase_conditions: $buyerAcceptsPurchaseConditions, buyer_accepts_terms_conditions: $buyerAcceptsTermsConditions) {
1029
+ buyer_accepts_purchase_conditions
1030
+ buyer_accepts_terms_conditions
1031
+ created_at
1032
+ updated_at
1033
+ id
1034
+ deleted_at
1035
+ success_url
1036
+ cancel_url
1037
+ payment_method
1038
+ email
1039
+ status
1040
+ checkout_url
1041
+ origin_payment_id
1042
+ billing_address {
1043
+ id
1044
+ first_name
1045
+ last_name
1046
+ phone_code
1047
+ phone
1048
+ company
1049
+ address1
1050
+ address2
1051
+ city
1052
+ province
1053
+ province_code
1054
+ country
1055
+ country_code
1056
+ zip
1057
+ }
1058
+ shipping_address {
1059
+ id
1060
+ first_name
1061
+ last_name
1062
+ phone_code
1063
+ phone
1064
+ company
1065
+ address1
1066
+ address2
1067
+ city
1068
+ province
1069
+ province_code
1070
+ country
1071
+ country_code
1072
+ zip
1073
+ }
1074
+ available_payment_methods {
1075
+ name
1076
+ }
1077
+ discount_code
1078
+ cart {
1079
+ cart_id
1080
+ customer_session_id
1081
+ shipping_country
1082
+ line_items {
1083
+ id
1084
+ supplier
1085
+ image {
1086
+ id
1087
+ url
1088
+ width
1089
+ height
1090
+ }
1091
+ sku
1092
+ barcode
1093
+ brand
1094
+ product_id
1095
+ title
1096
+ variant_id
1097
+ variant_title
1098
+ variant {
1099
+ option
1100
+ value
1101
+ }
1102
+ quantity
1103
+ price {
1104
+ amount
1105
+ currency_code
1106
+ discount
1107
+ compare_at
1108
+ compare_at_incl_taxes
1109
+ amount_incl_taxes
1110
+ tax_amount
1111
+ tax_rate
1112
+ }
1113
+ shipping {
1114
+ id
1115
+ name
1116
+ description
1117
+ price {
1118
+ amount
1119
+ currency_code
1120
+ amount_incl_taxes
1121
+ tax_amount
1122
+ tax_rate
1123
+ }
1124
+ }
1125
+ available_shippings {
1126
+ id
1127
+ name
1128
+ description
1129
+ country_code
1130
+ price {
1131
+ amount
1132
+ currency_code
1133
+ amount_incl_taxes
1134
+ tax_amount
1135
+ tax_rate
1136
+ }
1137
+ }
1138
+ }
1139
+ subtotal
1140
+ shipping
1141
+ currency
1142
+ available_shipping_countries
1143
+ }
1144
+ totals {
1145
+ currency_code
1146
+ subtotal
1147
+ shipping
1148
+ total
1149
+ taxes
1150
+ tax_rate
1151
+ discounts
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ `;
1157
+ var GET_CHECKOUT_QUERY = `
1158
+ query GetCheckout($checkoutId: String!) {
1159
+ Checkout {
1160
+ GetCheckout(checkout_id: $checkoutId) {
1161
+ buyer_accepts_purchase_conditions
1162
+ buyer_accepts_terms_conditions
1163
+ created_at
1164
+ updated_at
1165
+ id
1166
+ deleted_at
1167
+ success_url
1168
+ cancel_url
1169
+ payment_method
1170
+ email
1171
+ status
1172
+ checkout_url
1173
+ origin_payment_id
1174
+ billing_address {
1175
+ id
1176
+ first_name
1177
+ last_name
1178
+ phone_code
1179
+ phone
1180
+ company
1181
+ address1
1182
+ address2
1183
+ city
1184
+ province
1185
+ province_code
1186
+ country
1187
+ country_code
1188
+ zip
1189
+ }
1190
+ shipping_address {
1191
+ id
1192
+ first_name
1193
+ last_name
1194
+ phone_code
1195
+ phone
1196
+ company
1197
+ address1
1198
+ address2
1199
+ city
1200
+ province
1201
+ province_code
1202
+ country
1203
+ country_code
1204
+ zip
1205
+ }
1206
+ available_payment_methods {
1207
+ name
1208
+ }
1209
+ discount_code
1210
+ cart {
1211
+ cart_id
1212
+ customer_session_id
1213
+ shipping_country
1214
+ line_items {
1215
+ id
1216
+ supplier
1217
+ image {
1218
+ id
1219
+ url
1220
+ width
1221
+ height
1222
+ }
1223
+ sku
1224
+ barcode
1225
+ brand
1226
+ product_id
1227
+ title
1228
+ variant_id
1229
+ variant_title
1230
+ variant {
1231
+ option
1232
+ value
1233
+ }
1234
+ quantity
1235
+ price {
1236
+ amount
1237
+ currency_code
1238
+ discount
1239
+ compare_at
1240
+ compare_at_incl_taxes
1241
+ amount_incl_taxes
1242
+ tax_amount
1243
+ tax_rate
1244
+ }
1245
+ shipping {
1246
+ id
1247
+ name
1248
+ description
1249
+ price {
1250
+ amount
1251
+ currency_code
1252
+ amount_incl_taxes
1253
+ tax_amount
1254
+ tax_rate
1255
+ }
1256
+ }
1257
+ available_shippings {
1258
+ id
1259
+ name
1260
+ description
1261
+ country_code
1262
+ price {
1263
+ amount
1264
+ currency_code
1265
+ amount_incl_taxes
1266
+ tax_amount
1267
+ tax_rate
1268
+ }
1269
+ }
1270
+ }
1271
+ subtotal
1272
+ shipping
1273
+ currency
1274
+ available_shipping_countries
1275
+ }
1276
+ totals {
1277
+ currency_code
1278
+ subtotal
1279
+ shipping
1280
+ total
1281
+ taxes
1282
+ tax_rate
1283
+ discounts
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+ `;
1289
+ var GET_AVAILABLE_PAYMENT_METHODS_QUERY = `
1290
+ query Config {
1291
+ Payment {
1292
+ GetAvailablePaymentMethods {
1293
+ config {
1294
+ name
1295
+ type
1296
+ value
1297
+ }
1298
+ name
1299
+ }
1300
+ }
1301
+ }
1302
+ `;
1303
+ async function getCartGraphQLOptions(sponsorId) {
1304
+ const facade = globalThis.__VIO_FACADE__;
1305
+ if (facade && typeof facade.bootstrap === "function" && !facade.bootstrapCache) {
1306
+ try {
1307
+ await facade.bootstrap();
1308
+ } catch (err) {
1309
+ if (typeof console !== "undefined") {
1310
+ console.warn("[Vio] getCartGraphQLOptions bootstrap await failed:", err);
1311
+ }
1312
+ }
1313
+ }
1314
+ if (!Configuration.isInitialized) {
1315
+ await Configuration.whenReady();
1316
+ }
1317
+ const cfg = Configuration.isInitialized ? Configuration.get() : { apiKey: void 0, graphQLBase: "https://graph-ql-dev.vio.live" };
1318
+ const anyCfg = cfg;
1319
+ let commerceKey = anyCfg.apiKey;
1320
+ let sponsorResolved = false;
1321
+ if (sponsorId && typeof facade?.findSponsor === "function") {
1322
+ const sponsor = facade.findSponsor(sponsorId);
1323
+ if (sponsor?.commerce?.apiKey) {
1324
+ commerceKey = sponsor.commerce.apiKey;
1325
+ sponsorResolved = true;
1326
+ }
1327
+ }
1328
+ return {
1329
+ endpoint: anyCfg.graphQLBase || "https://graph-ql-dev.vio.live",
1330
+ apiKey: anyCfg.apiKey,
1331
+ commerceKey,
1332
+ sponsorResolved
1333
+ };
1334
+ }
1335
+ var CUSTOMER_SESSION_KEY = "vio.customer_session_id.v1";
1336
+ function getCustomerSessionId() {
1337
+ if (typeof localStorage === "undefined") return "cs_session_default";
1338
+ let id = localStorage.getItem(CUSTOMER_SESSION_KEY);
1339
+ if (!id) {
1340
+ id = "cs_" + Math.random().toString(36).slice(2, 11) + Date.now().toString(36);
1341
+ localStorage.setItem(CUSTOMER_SESSION_KEY, id);
1342
+ }
1343
+ return id;
1344
+ }
1345
+ async function executeCartGraphQL(query, variables, options = {}) {
1346
+ const resolvedOptions = typeof options === "number" ? await getCartGraphQLOptions(options) : !options.apiKey && !options.commerceKey ? { ...await getCartGraphQLOptions(), ...options } : options;
1347
+ const { endpoint = "https://graph-ql-dev.vio.live", apiKey, commerceKey } = resolvedOptions;
1348
+ const headers = { "Content-Type": "application/json" };
1349
+ if (apiKey) headers["X-API-Key"] = apiKey;
1350
+ if (commerceKey || apiKey) headers["Authorization"] = commerceKey || apiKey;
1351
+ const res = await fetch(endpoint, {
1352
+ method: "POST",
1353
+ headers,
1354
+ body: JSON.stringify({ query, variables })
1355
+ });
1356
+ const json = await res.json().catch(() => ({}));
1357
+ if (!res.ok || json.errors) {
1358
+ const errMsg = json.errors?.[0]?.message || `GraphQL request failed (HTTP ${res.status})`;
1359
+ throw new Error(errMsg);
1360
+ }
1361
+ return json;
1362
+ }
1363
+ async function createCart(customerSessionId, currency, shippingCountry, options) {
1364
+ const json = await executeCartGraphQL(
1365
+ CREATE_CART_MUTATION,
1366
+ {
1367
+ customerSessionId,
1368
+ currency: currency || getGlobalCurrency(),
1369
+ shippingCountry: shippingCountry || getGlobalCountryCode()
1370
+ },
1371
+ options
1372
+ );
1373
+ return json?.data?.Cart?.CreateCart;
1374
+ }
1375
+ async function addItem(cartId, lineItems, options) {
1376
+ const json = await executeCartGraphQL(ADD_ITEM_MUTATION, { cartId, lineItems }, options);
1377
+ return json?.data?.Cart?.AddItem;
1378
+ }
1379
+ async function deleteItem(cartId, cartItemId, options) {
1380
+ const json = await executeCartGraphQL(DELETE_ITEM_MUTATION, { cartId, cartItemId }, options);
1381
+ return json?.data?.Cart?.DeleteItem;
1382
+ }
1383
+ async function updateItem(cartId, cartItemId, shippingId, qty, options) {
1384
+ const json = await executeCartGraphQL(
1385
+ UPDATE_ITEM_MUTATION,
1386
+ { cartId, cartItemId, shippingId: shippingId || null, qty: qty != null ? qty : null },
1387
+ options
1388
+ );
1389
+ return json?.data?.Cart?.UpdateItem;
1390
+ }
1391
+ async function getCart(cartId, options) {
1392
+ const json = await executeCartGraphQL(GET_CART_QUERY, { cartId }, options);
1393
+ return json?.data?.Cart?.GetCart;
1394
+ }
1395
+ async function getLineItemsBySupplier(cartId, options) {
1396
+ const json = await executeCartGraphQL(GET_LINE_ITEMS_BY_SUPPLIER_QUERY, { cartId }, options);
1397
+ return json?.data?.Cart?.GetLineItemsBySupplier;
1398
+ }
1399
+ async function updateShippingsBySupplier(cartId, data, options) {
1400
+ const json = await executeCartGraphQL(
1401
+ UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION,
1402
+ { cartId, data },
1403
+ options
1404
+ );
1405
+ return json?.data?.Cart?.UpdateShippingsBySupplier;
1406
+ }
1407
+ async function createCheckout(cartId, options) {
1408
+ const json = await executeCartGraphQL(CREATE_CHECKOUT_MUTATION, { cartId }, options);
1409
+ return json?.data?.Checkout?.CreateCheckout;
1410
+ }
1411
+ async function updateCheckout(variables, options) {
1412
+ const json = await executeCartGraphQL(UPDATE_CHECKOUT_MUTATION, variables, options);
1413
+ return json?.data?.Checkout?.UpdateCheckout;
1414
+ }
1415
+ async function getCheckout(checkoutId, options) {
1416
+ const json = await executeCartGraphQL(GET_CHECKOUT_QUERY, { checkoutId }, options);
1417
+ return json?.data?.Checkout?.GetCheckout;
1418
+ }
1419
+ async function getAvailablePaymentMethods(options) {
1420
+ const json = await executeCartGraphQL(GET_AVAILABLE_PAYMENT_METHODS_QUERY, {}, options);
1421
+ return json?.data?.Payment?.GetAvailablePaymentMethods;
1422
+ }
1423
+ var CREATE_PAYMENT_STRIPE_MUTATION = `
1424
+ mutation CreatePaymentStripe($checkoutId: String!, $successUrl: String!, $paymentMethod: String!, $email: String!) {
1425
+ Payment {
1426
+ CreatePaymentStripe(checkout_id: $checkoutId, success_url: $successUrl, payment_method: $paymentMethod, email: $email) {
1427
+ order_id
1428
+ checkout_url
1429
+ }
1430
+ }
1431
+ }
1432
+ `;
1433
+ async function createPaymentStripe(variables, options) {
1434
+ const json = await executeCartGraphQL(CREATE_PAYMENT_STRIPE_MUTATION, variables, options);
1435
+ return json?.data?.Payment?.CreatePaymentStripe;
1436
+ }
1437
+ var CREATE_PAYMENT_APPLE_PAY_MUTATION = `
1438
+ mutation CreatePaymentApplePay($checkoutId: String!) {
1439
+ Payment {
1440
+ CreatePaymentApplePay(checkout_id: $checkoutId) {
1441
+ gateway
1442
+ gateway_merchant_id
1443
+ }
1444
+ }
1445
+ }
1446
+ `;
1447
+ async function createPaymentApplePay(variables, options) {
1448
+ const json = await executeCartGraphQL(CREATE_PAYMENT_APPLE_PAY_MUTATION, variables, options);
1449
+ return json?.data?.Payment?.CreatePaymentApplePay;
1450
+ }
1451
+ var CONFIRM_PAYMENT_APPLE_PAY_MUTATION = `
1452
+ mutation ConfirmPaymentApplePay($checkoutId: String!, $applePayToken: String!, $email: String, $shippingAddress: ApplePayAddressInput) {
1453
+ Payment {
1454
+ ConfirmPaymentApplePay(checkout_id: $checkoutId, apple_pay_token: $applePayToken, email: $email, shipping_address: $shippingAddress) {
1455
+ order_id
1456
+ status
1457
+ }
1458
+ }
1459
+ }
1460
+ `;
1461
+ async function confirmPaymentApplePay(variables, options) {
1462
+ const json = await executeCartGraphQL(CONFIRM_PAYMENT_APPLE_PAY_MUTATION, variables, options);
1463
+ return json?.data?.Payment?.ConfirmPaymentApplePay;
1464
+ }
1465
+
1466
+ // src/core/checkout/payments/vipps.ts
1467
+ var CREATE_PAYMENT_VIPPS_MUTATION = `
1468
+ mutation CreatePaymentVipps($checkoutId: String!, $email: String!, $returnUrl: String!) {
1469
+ Payment {
1470
+ CreatePaymentVipps(checkout_id: $checkoutId, email: $email, return_url: $returnUrl) {
1471
+ payment_url
1472
+ }
1473
+ }
1474
+ }
1475
+ `;
1476
+ var GET_VIPPS_STATUS_QUERY = `
1477
+ query GetVippsStatus($checkoutId: String!) {
1478
+ Payment {
1479
+ GetVippsStatus(checkout_id: $checkoutId) {
1480
+ state
1481
+ }
1482
+ }
1483
+ }
1484
+ `;
1485
+ async function createPaymentVipps(variables, options) {
1486
+ const json = await executeCartGraphQL(CREATE_PAYMENT_VIPPS_MUTATION, variables, options);
1487
+ return json?.data?.Payment?.CreatePaymentVipps ?? null;
1488
+ }
1489
+ async function getVippsStatus(variables, options) {
1490
+ const json = await executeCartGraphQL(GET_VIPPS_STATUS_QUERY, variables, options);
1491
+ return json?.data?.Payment?.GetVippsStatus ?? null;
1492
+ }
1493
+
1494
+ // src/core/cart/cart-manager.ts
1495
+ var STORAGE_KEY = "vio.cart.v1";
1496
+ function lineId() {
1497
+ return "li_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
1498
+ }
1499
+ function normId(val) {
1500
+ if (val === null || val === void 0 || val === 0 || val === "0" || val === "null" || val === "undefined") {
1501
+ return "";
1502
+ }
1503
+ return String(val).trim();
1504
+ }
1505
+ var CartManager = class extends EventTarget {
1506
+ constructor() {
1507
+ super();
1508
+ /** In-flight CreateCart per sponsor — two quick adds must share ONE backend
1509
+ * cart (two CreateCarts split the items and one silently disappears). */
1510
+ this.cartIdPromises = /* @__PURE__ */ new Map();
1511
+ /** Monotonic mutation counter per sponsor + last applied response, so a
1512
+ * slow response can't overwrite the state of a newer one. */
1513
+ this.mutationSeq = /* @__PURE__ */ new Map();
1514
+ this.appliedSeq = /* @__PURE__ */ new Map();
1515
+ /** Quantity intents for lines whose backend id hasn't arrived yet — replayed
1516
+ * when the AddItem response reconciles (keyed by product::variant). */
1517
+ this.pendingQtyIntents = /* @__PURE__ */ new Map();
1518
+ this.cartsBySponsor = this.loadFromStorage();
1519
+ }
1520
+ nextSeq(sponsorId) {
1521
+ const n = (this.mutationSeq.get(sponsorId) ?? 0) + 1;
1522
+ this.mutationSeq.set(sponsorId, n);
1523
+ return n;
1524
+ }
1525
+ /** Apply a backend response only if no newer response was applied already. */
1526
+ applyBackendCart(sponsorId, backendCart, seq) {
1527
+ if (seq < (this.appliedSeq.get(sponsorId) ?? 0)) return;
1528
+ this.appliedSeq.set(sponsorId, seq);
1529
+ this.updateFromBackendCart(sponsorId, backendCart);
1530
+ }
1531
+ /** Surface a cart sync problem to hosts/UI (and the console). */
1532
+ emitError(message, err) {
1533
+ if (typeof console !== "undefined") console.warn("[CartManager]", message, err ?? "");
1534
+ this.dispatchEvent(new CustomEvent("error", { detail: { message } }));
1535
+ if (typeof document !== "undefined") {
1536
+ document.dispatchEvent(
1537
+ new CustomEvent("vio:cart-error", { bubbles: true, detail: { message } })
1538
+ );
1539
+ }
1540
+ }
1541
+ // MARK: - Backend cart sync (Vio Commerce GraphQL)
1542
+ /** Ensure the sponsor cart exists server-side; returns its backend cart id. */
1543
+ async ensureCartId(sponsorId, currency, shippingCountry) {
1544
+ const activeCurrency = currency || getGlobalCurrency();
1545
+ const activeCountry = shippingCountry || getGlobalCountryCode();
1546
+ let cart = this.cartsBySponsor.get(sponsorId);
1547
+ if (!cart) {
1548
+ cart = { sponsorId, items: [], currency: activeCurrency, shippingCountry: activeCountry };
1549
+ this.cartsBySponsor.set(sponsorId, cart);
1550
+ } else {
1551
+ cart.shippingCountry = activeCountry;
1552
+ }
1553
+ if (cart.cartId) return cart.cartId;
1554
+ const inFlight = this.cartIdPromises.get(sponsorId);
1555
+ if (inFlight) return inFlight;
1556
+ const creation = (async () => {
1557
+ const opts = await getCartGraphQLOptions(sponsorId);
1558
+ if (!opts.sponsorResolved) {
1559
+ this.emitError(`no commerce key resolved for sponsor ${sponsorId} \u2014 cart not synced`);
1560
+ return void 0;
1561
+ }
1562
+ const customerSessionId = getCustomerSessionId();
1563
+ try {
1564
+ const backendCart = await createCart(
1565
+ customerSessionId,
1566
+ activeCurrency,
1567
+ activeCountry,
1568
+ opts
1569
+ );
1570
+ if (backendCart?.cart_id) {
1571
+ cart.cartId = backendCart.cart_id;
1572
+ if (Array.isArray(backendCart.line_items) && backendCart.line_items.length > 0) {
1573
+ this.updateFromBackendCart(sponsorId, backendCart);
1574
+ } else {
1575
+ if (backendCart.currency) cart.currency = backendCart.currency;
1576
+ if (backendCart.shipping_country) cart.shippingCountry = backendCart.shipping_country;
1577
+ if (backendCart.subtotal != null) cart.subtotal = backendCart.subtotal;
1578
+ if (backendCart.shipping != null) cart.shipping = backendCart.shipping;
1579
+ if (backendCart.available_shipping_countries) {
1580
+ cart.availableShippingCountries = backendCart.available_shipping_countries;
1581
+ }
1582
+ this.persist();
1583
+ this.emit();
1584
+ }
1585
+ }
1586
+ } catch (err) {
1587
+ this.emitError("createCart failed", err);
1588
+ }
1589
+ return cart.cartId;
1590
+ })().finally(() => {
1591
+ this.cartIdPromises.delete(sponsorId);
1592
+ });
1593
+ this.cartIdPromises.set(sponsorId, creation);
1594
+ return creation;
1595
+ }
1596
+ /** Overwrite local cart state with the backend cart response (source of truth). */
1597
+ updateFromBackendCart(sponsorId, backendCart) {
1598
+ const cart = this.cartsBySponsor.get(sponsorId);
1599
+ if (!cart) return;
1600
+ if (backendCart.cart_id) cart.cartId = backendCart.cart_id;
1601
+ if (backendCart.currency) cart.currency = backendCart.currency;
1602
+ if (backendCart.shipping_country) cart.shippingCountry = backendCart.shipping_country;
1603
+ if (backendCart.subtotal != null) cart.subtotal = backendCart.subtotal;
1604
+ if (backendCart.shipping != null) cart.shipping = backendCart.shipping;
1605
+ if (backendCart.available_shipping_countries) {
1606
+ cart.availableShippingCountries = backendCart.available_shipping_countries;
1607
+ }
1608
+ if (Array.isArray(backendCart.line_items)) {
1609
+ const existingItems = cart.items || [];
1610
+ if (backendCart.line_items.length === 0 && existingItems.length > 0) {
1611
+ this.persist();
1612
+ this.emit();
1613
+ return;
1614
+ }
1615
+ cart.items = backendCart.line_items.map((li) => {
1616
+ const urlFromBackend = (typeof li.image === "string" ? li.image : li.image?.url) || li.imageUrl || li.image_url || "";
1617
+ const matchedLocal = existingItems.find((existing) => {
1618
+ const p1 = normId(existing.productId);
1619
+ const p2 = normId(li.product_id);
1620
+ const v1 = normId(existing.variantId);
1621
+ const v2 = normId(li.variant_id);
1622
+ if (p1 && p2 && p1 === p2 && v1 === v2) return true;
1623
+ if (v1 && v2 && v1 === v2) return true;
1624
+ if (p1 && p2 && p1 === p2 && !v1 && !v2) return true;
1625
+ const n1 = (existing.name || "").trim().toLowerCase();
1626
+ const n2 = (li.title || "").trim().toLowerCase();
1627
+ if (n1 && n2 && n1 === n2 && v1 === v2) return true;
1628
+ return false;
1629
+ });
1630
+ const imageUrl = urlFromBackend || matchedLocal?.imageUrl || "";
1631
+ return {
1632
+ id: li.id,
1633
+ cartItemId: li.id,
1634
+ productId: li.product_id,
1635
+ sponsorId,
1636
+ variantId: li.variant_id != null ? Number(li.variant_id) : void 0,
1637
+ brand: li.brand || matchedLocal?.brand || "",
1638
+ name: li.title ? li.variant_title ? `${li.title} \u2014 ${li.variant_title}` : li.title : matchedLocal?.name || "",
1639
+ unitPrice: li.price?.amount_incl_taxes ?? li.price?.amount ?? matchedLocal?.unitPrice ?? 0,
1640
+ currency: li.price?.currency_code || backendCart.currency || matchedLocal?.currency || "NOK",
1641
+ imageUrl,
1642
+ quantity: li.quantity,
1643
+ shipping: li.shipping,
1644
+ availableShippings: li.available_shippings
1645
+ };
1646
+ });
1647
+ }
1648
+ this.persist();
1649
+ this.emit();
1650
+ if (this.pendingQtyIntents.size > 0) {
1651
+ const currentCart = this.cartsBySponsor.get(sponsorId);
1652
+ for (const [key, qty] of [...this.pendingQtyIntents]) {
1653
+ const line = currentCart?.items.find(
1654
+ (i) => this.intentKey(i.productId, i.variantId) === key && i.cartItemId
1655
+ );
1656
+ if (line) {
1657
+ this.pendingQtyIntents.delete(key);
1658
+ if (line.quantity !== qty) {
1659
+ this.updateQuantity(line.cartItemId, sponsorId, qty);
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1665
+ // MARK: - Mutations
1666
+ /** Add a Product DTO to a sponsor's cart. Bumps quantity if already present.
1667
+ * When a variantId is given, the line uses the VARIANT's price / image / title
1668
+ * (not the product base), so different combinations are priced correctly. */
1669
+ addProduct(opts) {
1670
+ const variant = opts.variantId != null ? opts.product.variants?.find((v) => v.id === opts.variantId) : void 0;
1671
+ const price = variant?.price ?? opts.product.price;
1672
+ const unitPrice = price.amount_incl_taxes ?? price.amount ?? 0;
1673
+ const imageUrl = variant?.images?.[0]?.url || primaryImageUrl(opts.product) || "";
1674
+ const name = variant?.title ? `${opts.product.title} \u2014 ${variant.title}` : opts.product.title;
1675
+ return this.addManual({
1676
+ productId: opts.product.id,
1677
+ sponsorId: opts.sponsorId,
1678
+ sponsorName: opts.sponsorName,
1679
+ brand: opts.product.brand ?? "",
1680
+ name,
1681
+ unitPrice,
1682
+ currency: price.currency_code,
1683
+ imageUrl,
1684
+ quantity: opts.quantity,
1685
+ variantId: opts.variantId
1686
+ });
1687
+ }
1688
+ /** Add a manually-specified line item (no Product DTO). */
1689
+ addManual(opts) {
1690
+ const qty = Math.max(1, opts.quantity ?? 1);
1691
+ let cart = this.cartsBySponsor.get(opts.sponsorId);
1692
+ if (!cart) {
1693
+ cart = {
1694
+ sponsorId: opts.sponsorId,
1695
+ sponsorName: opts.sponsorName,
1696
+ items: [],
1697
+ currency: opts.currency
1698
+ };
1699
+ this.cartsBySponsor.set(opts.sponsorId, cart);
1700
+ } else if (opts.sponsorName && !cart.sponsorName) {
1701
+ cart.sponsorName = opts.sponsorName;
1702
+ }
1703
+ const existing = cart.items.find((i) => {
1704
+ const p1 = normId(i.productId);
1705
+ const p2 = normId(opts.productId);
1706
+ const v1 = normId(i.variantId);
1707
+ const v2 = normId(opts.variantId);
1708
+ if (p1 && p2 && p1 === p2 && v1 === v2) return true;
1709
+ if (v1 && v2 && v1 === v2) return true;
1710
+ if (p1 && p2 && p1 === p2 && !v1 && !v2) return true;
1711
+ const n1 = (i.name || "").trim().toLowerCase();
1712
+ const n2 = (opts.name || "").trim().toLowerCase();
1713
+ if (n1 && n2 && n1 === n2 && v1 === v2) return true;
1714
+ return false;
1715
+ });
1716
+ if (existing) {
1717
+ existing.quantity += qty;
1718
+ } else {
1719
+ const item = {
1720
+ id: lineId(),
1721
+ productId: opts.productId,
1722
+ sponsorId: opts.sponsorId,
1723
+ variantId: opts.variantId,
1724
+ brand: opts.brand,
1725
+ name: opts.name,
1726
+ unitPrice: opts.unitPrice,
1727
+ currency: opts.currency,
1728
+ imageUrl: opts.imageUrl,
1729
+ quantity: qty
1730
+ };
1731
+ cart.items.push(item);
1732
+ }
1733
+ this.persist();
1734
+ this.emit();
1735
+ const addedItem = existing ?? cart.items[cart.items.length - 1];
1736
+ const addedLineId = addedItem.id;
1737
+ const seq = this.nextSeq(opts.sponsorId);
1738
+ void (async () => {
1739
+ try {
1740
+ const cartId = await this.ensureCartId(opts.sponsorId, opts.currency || void 0);
1741
+ if (cartId) {
1742
+ const gqlOpts = await getCartGraphQLOptions(opts.sponsorId);
1743
+ const lineItemsInput = [
1744
+ {
1745
+ product_id: Number(opts.productId || 0),
1746
+ quantity: qty,
1747
+ variant_id: opts.variantId != null ? Number(opts.variantId) : null
1748
+ }
1749
+ ];
1750
+ const resCart = await addItem(cartId, lineItemsInput, gqlOpts);
1751
+ if (resCart && Array.isArray(resCart.line_items)) {
1752
+ this.applyBackendCart(opts.sponsorId, resCart, seq);
1753
+ }
1754
+ }
1755
+ } catch (err) {
1756
+ const c = this.cartsBySponsor.get(opts.sponsorId);
1757
+ const line = c?.items.find((i) => i.id === addedLineId);
1758
+ if (c && line) {
1759
+ line.quantity -= qty;
1760
+ if (line.quantity <= 0) c.items = c.items.filter((i) => i !== line);
1761
+ if (c.items.length === 0) this.cartsBySponsor.delete(opts.sponsorId);
1762
+ this.persist();
1763
+ this.emit();
1764
+ }
1765
+ this.emitError("AddItem rejected by the backend \u2014 item removed from the cart", err);
1766
+ }
1767
+ })();
1768
+ return addedItem;
1769
+ }
1770
+ intentKey(productId, variantId) {
1771
+ return `${normId(productId)}::${normId(variantId)}`;
1772
+ }
1773
+ updateQuantity(lineItemId, sponsorId, quantity) {
1774
+ const cart = this.cartsBySponsor.get(sponsorId);
1775
+ if (!cart) return;
1776
+ const item = cart.items.find((i) => i.id === lineItemId || i.cartItemId === lineItemId);
1777
+ if (!item) return;
1778
+ const targetItemId = item.cartItemId;
1779
+ if (quantity <= 0) {
1780
+ cart.items = cart.items.filter((i) => i !== item);
1781
+ } else {
1782
+ item.quantity = quantity;
1783
+ }
1784
+ if (cart.items.length === 0) this.cartsBySponsor.delete(sponsorId);
1785
+ this.persist();
1786
+ this.emit();
1787
+ if (!cart.cartId) return;
1788
+ if (!targetItemId) {
1789
+ this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId), quantity);
1790
+ return;
1791
+ }
1792
+ const backendCartId = cart.cartId;
1793
+ const seq = this.nextSeq(sponsorId);
1794
+ void (async () => {
1795
+ try {
1796
+ const gqlOpts = await getCartGraphQLOptions(sponsorId);
1797
+ const resCart = quantity <= 0 ? await deleteItem(backendCartId, targetItemId, gqlOpts) : await updateItem(backendCartId, targetItemId, null, quantity, gqlOpts);
1798
+ if (resCart && Array.isArray(resCart.line_items)) {
1799
+ this.applyBackendCart(sponsorId, resCart, seq);
1800
+ }
1801
+ } catch (err) {
1802
+ this.emitError("Update/DeleteItem failed \u2014 cart re-synced from backend", err);
1803
+ void this.fetchCartFromBackend(sponsorId);
1804
+ }
1805
+ })();
1806
+ }
1807
+ removeItem(lineItemId, sponsorId) {
1808
+ this.updateQuantity(lineItemId, sponsorId, 0);
1809
+ }
1810
+ clearSponsorCart(sponsorId) {
1811
+ if (this.cartsBySponsor.delete(sponsorId)) {
1812
+ this.persist();
1813
+ this.emit();
1814
+ }
1815
+ }
1816
+ clearAllCarts() {
1817
+ if (this.cartsBySponsor.size === 0) return;
1818
+ this.cartsBySponsor.clear();
1819
+ this.persist();
1820
+ this.emit();
1821
+ }
1822
+ /** Refresh the local cart from the backend (source of truth). */
1823
+ async fetchCartFromBackend(sponsorId) {
1824
+ const cart = this.cartsBySponsor.get(sponsorId);
1825
+ if (!cart?.cartId) return null;
1826
+ const gqlOpts = await getCartGraphQLOptions(sponsorId);
1827
+ const resCart = await getCart(cart.cartId, gqlOpts);
1828
+ if (resCart) this.updateFromBackendCart(sponsorId, resCart);
1829
+ return resCart;
1830
+ }
1831
+ /** Line items grouped by supplier (shipping is per-supplier server-side). */
1832
+ async fetchLineItemsBySupplier(sponsorId) {
1833
+ const cart = this.cartsBySponsor.get(sponsorId);
1834
+ if (!cart?.cartId) return null;
1835
+ const gqlOpts = await getCartGraphQLOptions(sponsorId);
1836
+ return getLineItemsBySupplier(cart.cartId, gqlOpts);
1837
+ }
1838
+ /** Set the chosen shipping option per supplier on the backend cart. */
1839
+ async updateShippingsBySupplier(sponsorId, data) {
1840
+ const cart = this.cartsBySponsor.get(sponsorId);
1841
+ if (!cart?.cartId) return null;
1842
+ const gqlOpts = await getCartGraphQLOptions(sponsorId);
1843
+ const resCart = await updateShippingsBySupplier(cart.cartId, data, gqlOpts);
1844
+ if (resCart && Array.isArray(resCart.line_items)) {
1845
+ this.updateFromBackendCart(sponsorId, resCart);
1846
+ }
1847
+ return resCart;
1848
+ }
1849
+ // MARK: - Read accessors
1850
+ getAllCarts() {
1851
+ return new Map(this.cartsBySponsor);
1852
+ }
1853
+ getCart(sponsorId) {
1854
+ return this.cartsBySponsor.get(sponsorId);
1855
+ }
1856
+ get itemCount() {
1857
+ let count = 0;
1858
+ for (const cart of this.cartsBySponsor.values()) {
1859
+ for (const item of cart.items) count += item.quantity;
1860
+ }
1861
+ return count;
1862
+ }
1863
+ subtotalForSponsor(sponsorId) {
1864
+ const cart = this.cartsBySponsor.get(sponsorId);
1865
+ if (!cart) return 0;
1866
+ return cart.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
1867
+ }
1868
+ get totalAcrossSponsors() {
1869
+ let sum = 0;
1870
+ for (const cart of this.cartsBySponsor.values()) {
1871
+ for (const item of cart.items) sum += item.unitPrice * item.quantity;
1872
+ }
1873
+ return sum;
1874
+ }
1875
+ get primaryCurrency() {
1876
+ for (const cart of this.cartsBySponsor.values()) return cart.currency;
1877
+ return "NOK";
1878
+ }
1879
+ formatTotal() {
1880
+ return formatPrice(this.totalAcrossSponsors, this.primaryCurrency);
1881
+ }
1882
+ // MARK: - Persistence
1883
+ persist() {
1884
+ if (typeof localStorage === "undefined") return;
1885
+ try {
1886
+ const obj = {};
1887
+ for (const [k, v] of this.cartsBySponsor) obj[String(k)] = v;
1888
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(obj));
1889
+ } catch {
1890
+ }
1891
+ }
1892
+ loadFromStorage() {
1893
+ if (typeof localStorage === "undefined") return /* @__PURE__ */ new Map();
1894
+ try {
1895
+ const raw = localStorage.getItem(STORAGE_KEY);
1896
+ if (!raw) return /* @__PURE__ */ new Map();
1897
+ const obj = JSON.parse(raw);
1898
+ const map = /* @__PURE__ */ new Map();
1899
+ for (const [k, v] of Object.entries(obj)) {
1900
+ const id = parseInt(k, 10);
1901
+ if (!Number.isNaN(id) && v && Array.isArray(v.items)) map.set(id, v);
1902
+ }
1903
+ return map;
1904
+ } catch {
1905
+ return /* @__PURE__ */ new Map();
1906
+ }
1907
+ }
1908
+ emit() {
1909
+ const detail = {
1910
+ cartsBySponsor: this.getAllCarts(),
1911
+ itemCount: this.itemCount
1912
+ };
1913
+ this.dispatchEvent(new CustomEvent("change", { detail }));
1914
+ }
1915
+ };
1916
+
1917
+ // src/core/checkout/payments/apple-pay.ts
1918
+ var STRIPE_JS_URL = "https://js.stripe.com/v3";
1919
+ var stripeJsPromise = null;
1920
+ function preloadStripeJs() {
1921
+ if (typeof window === "undefined") return;
1922
+ void loadStripeJs().catch(() => {
1923
+ });
1924
+ }
1925
+ function loadStripeJs() {
1926
+ if (stripeJsPromise) return stripeJsPromise;
1927
+ if (typeof document === "undefined") {
1928
+ return Promise.reject(new Error("[VioApplePay] Stripe.js requires a browser environment"));
1929
+ }
1930
+ stripeJsPromise = new Promise((resolve, reject) => {
1931
+ const w = window;
1932
+ if (w.Stripe) {
1933
+ resolve(w.Stripe);
1934
+ return;
1935
+ }
1936
+ const existing = document.querySelector(
1937
+ `script[src^="${STRIPE_JS_URL}"]`
1938
+ );
1939
+ if (existing) {
1940
+ existing.addEventListener("load", () => resolve(w.Stripe));
1941
+ existing.addEventListener(
1942
+ "error",
1943
+ () => reject(new Error("[VioApplePay] Failed to load Stripe.js"))
1944
+ );
1945
+ return;
1946
+ }
1947
+ const script = document.createElement("script");
1948
+ script.src = STRIPE_JS_URL;
1949
+ script.async = true;
1950
+ script.addEventListener("load", () => resolve(w.Stripe));
1951
+ script.addEventListener(
1952
+ "error",
1953
+ () => reject(new Error("[VioApplePay] Failed to load Stripe.js"))
1954
+ );
1955
+ document.head.appendChild(script);
1956
+ });
1957
+ return stripeJsPromise;
1958
+ }
1959
+ async function getStripe(config) {
1960
+ const Stripe = await loadStripeJs();
1961
+ return Stripe(
1962
+ config.publishableKey,
1963
+ config.connectedAccount ? { stripeAccount: config.connectedAccount } : void 0
1964
+ );
1965
+ }
1966
+ function buildRequestOptions(config) {
1967
+ const ship = config.shippingOptions;
1968
+ const preselectedShipping = ship && ship.length > 0 ? ship[0].amount : 0;
1969
+ return {
1970
+ country: config.country,
1971
+ currency: config.currency.toLowerCase(),
1972
+ total: { label: config.label, amount: config.amount + preselectedShipping },
1973
+ requestPayerName: config.requestPayerName ?? true,
1974
+ requestPayerEmail: config.requestPayerEmail ?? true,
1975
+ requestShipping: ship && ship.length > 0 ? true : config.requestShipping ?? false,
1976
+ ...ship && ship.length > 0 ? { shippingOptions: ship } : {},
1977
+ disableWallets: ["link", "googlePay"]
1978
+ };
1979
+ }
1980
+ async function checkApplePayAvailability(config) {
1981
+ if (typeof window === "undefined") return false;
1982
+ if (typeof window.ApplePaySession === "undefined") {
1983
+ return false;
1984
+ }
1985
+ if (!config.publishableKey) return false;
1986
+ try {
1987
+ const stripe = await getStripe(config);
1988
+ const pr = stripe.paymentRequest(buildRequestOptions(config));
1989
+ const result = await pr.canMakePayment();
1990
+ return !!result?.applePay;
1991
+ } catch (err) {
1992
+ if (typeof console !== "undefined") {
1993
+ console.warn("[VioApplePay] availability check failed:", err);
1994
+ }
1995
+ return false;
1996
+ }
1997
+ }
1998
+ async function showApplePaySheet(config) {
1999
+ if (!config.publishableKey) {
2000
+ throw new Error("[VioApplePay] publishableKey is required");
2001
+ }
2002
+ const stripe = await getStripe(config);
2003
+ const pr = stripe.paymentRequest(buildRequestOptions(config));
2004
+ const canMake = await pr.canMakePayment();
2005
+ if (!canMake?.applePay) {
2006
+ throw new Error("[VioApplePay] Apple Pay is not available on this device");
2007
+ }
2008
+ return new Promise((resolve, reject) => {
2009
+ let settled = false;
2010
+ pr.on("shippingaddresschange", (raw) => {
2011
+ const ev = raw;
2012
+ ev.updateWith({ status: "success" });
2013
+ });
2014
+ pr.on("paymentmethod", (raw) => {
2015
+ const ev = raw;
2016
+ settled = true;
2017
+ const result = {
2018
+ paymentMethodId: ev.paymentMethod.id,
2019
+ payerName: ev.payerName,
2020
+ payerEmail: ev.payerEmail,
2021
+ payerPhone: ev.payerPhone,
2022
+ shippingAddress: ev.shippingAddress
2023
+ };
2024
+ void (async () => {
2025
+ try {
2026
+ const confirmation = config.onAuthorize ? await config.onAuthorize(result) : void 0;
2027
+ ev.complete("success");
2028
+ resolve({ ...result, confirmation });
2029
+ } catch (err) {
2030
+ ev.complete("fail");
2031
+ reject(err);
2032
+ }
2033
+ })();
2034
+ });
2035
+ pr.on("cancel", () => {
2036
+ if (!settled) resolve(null);
2037
+ });
2038
+ pr.show();
2039
+ });
2040
+ }
2041
+ async function prepareApplePay(config) {
2042
+ const unavailable = (msg) => ({
2043
+ available: false,
2044
+ show: () => Promise.reject(new Error(msg))
2045
+ });
2046
+ if (typeof window === "undefined" || !config.publishableKey) {
2047
+ return unavailable("[VioApplePay] unavailable");
2048
+ }
2049
+ if (typeof window.ApplePaySession === "undefined") {
2050
+ return unavailable("[VioApplePay] ApplePaySession not available");
2051
+ }
2052
+ try {
2053
+ const stripe = await getStripe(config);
2054
+ const pr = stripe.paymentRequest(buildRequestOptions(config));
2055
+ if (config.shippingOptions && config.shippingOptions.length > 0) {
2056
+ pr.on("shippingoptionchange", (raw) => {
2057
+ const ev = raw;
2058
+ const opt = config.shippingOptions?.find((o) => o.id === ev.shippingOption?.id);
2059
+ ev.updateWith({
2060
+ status: "success",
2061
+ total: { label: config.label, amount: config.amount + (opt?.amount ?? 0) }
2062
+ });
2063
+ });
2064
+ }
2065
+ pr.on("shippingaddresschange", (raw) => {
2066
+ const ev = raw;
2067
+ ev.updateWith({ status: "success" });
2068
+ });
2069
+ const result = await pr.canMakePayment();
2070
+ if (!result?.applePay) return unavailable("[VioApplePay] Apple Pay not available");
2071
+ return {
2072
+ available: true,
2073
+ show() {
2074
+ return new Promise((resolve, reject) => {
2075
+ let settled = false;
2076
+ pr.on("paymentmethod", (raw) => {
2077
+ const ev = raw;
2078
+ settled = true;
2079
+ const res = {
2080
+ paymentMethodId: ev.paymentMethod.id,
2081
+ payerName: ev.payerName,
2082
+ payerEmail: ev.payerEmail,
2083
+ payerPhone: ev.payerPhone,
2084
+ shippingAddress: ev.shippingAddress
2085
+ };
2086
+ void (async () => {
2087
+ try {
2088
+ const confirmation = config.onAuthorize ? await config.onAuthorize(res) : void 0;
2089
+ ev.complete("success");
2090
+ resolve({ ...res, confirmation });
2091
+ } catch (err) {
2092
+ ev.complete("fail");
2093
+ reject(err);
2094
+ }
2095
+ })();
2096
+ });
2097
+ pr.on("cancel", () => {
2098
+ if (!settled) reject(new Error("[VioApplePay] User cancelled Apple Pay"));
2099
+ });
2100
+ pr.show();
2101
+ });
2102
+ }
2103
+ };
2104
+ } catch (err) {
2105
+ if (typeof console !== "undefined") {
2106
+ console.warn("[VioApplePay] prepare failed:", err);
2107
+ }
2108
+ return unavailable("[VioApplePay] prepare failed");
2109
+ }
2110
+ }
2111
+
2112
+ // src/core/checkout/payments/klarna.ts
2113
+ var KLARNA_SDK_URL = "https://js.klarna.com/web-sdk/v2/klarna.mjs";
2114
+ var sdkCache = /* @__PURE__ */ new Map();
2115
+ function loadKlarnaSdk(config) {
2116
+ if (typeof window === "undefined") {
2117
+ return Promise.reject(new Error("[VioKlarna] requires a browser environment"));
2118
+ }
2119
+ if (!config.clientId) {
2120
+ return Promise.reject(new Error("[VioKlarna] clientId is required"));
2121
+ }
2122
+ const cacheKey = `${config.clientId}:${config.environment ?? "default"}`;
2123
+ const cached = sdkCache.get(cacheKey);
2124
+ if (cached) return cached;
2125
+ const url = KLARNA_SDK_URL;
2126
+ const promise = import(
2127
+ /* @vite-ignore */
2128
+ url
2129
+ ).then((mod) => {
2130
+ const factory = mod.KlarnaSDK ?? mod.default;
2131
+ if (typeof factory !== "function") {
2132
+ throw new Error("[VioKlarna] KlarnaSDK factory not found in module");
2133
+ }
2134
+ return factory({
2135
+ clientId: config.clientId,
2136
+ products: ["PAYMENT"],
2137
+ ...config.locale ? { locale: config.locale } : {},
2138
+ ...config.environment ? { environment: config.environment } : {}
2139
+ });
2140
+ }).catch((err) => {
2141
+ sdkCache.delete(cacheKey);
2142
+ throw err;
2143
+ });
2144
+ sdkCache.set(cacheKey, promise);
2145
+ return promise;
2146
+ }
2147
+ async function isKlarnaAvailable(config) {
2148
+ if (typeof window === "undefined" || !config.clientId) return false;
2149
+ try {
2150
+ await loadKlarnaSdk(config);
2151
+ return true;
2152
+ } catch (err) {
2153
+ if (typeof console !== "undefined") {
2154
+ console.warn("[VioKlarna] availability check failed:", err);
2155
+ }
2156
+ return false;
2157
+ }
2158
+ }
2159
+ function findAuthToken(value, depth = 0) {
2160
+ if (!value || depth > 4 || typeof value !== "object") return void 0;
2161
+ const o = value;
2162
+ for (const key of ["authorizationToken", "authorization_token", "authToken"]) {
2163
+ if (typeof o[key] === "string" && o[key].length > 0) return o[key];
2164
+ }
2165
+ for (const k of Object.keys(o)) {
2166
+ const found = findAuthToken(o[k], depth + 1);
2167
+ if (found) return found;
2168
+ }
2169
+ return void 0;
2170
+ }
2171
+ async function mountKlarnaExpressButton(opts) {
2172
+ const sdk = await loadKlarnaSdk(opts.config);
2173
+ const button = sdk.Payment.button({
2174
+ id: "vio-klarna-express",
2175
+ // `PAY` = one-time purchase (vs BUY/SUBSCRIBE). Must be an array.
2176
+ intents: ["PAY"],
2177
+ // initiationMode belongs on the BUTTON config (per Klarna Web SDK docs),
2178
+ // NOT inside the initiate() data — when set only in initiate() the SDK
2179
+ // falls back to its default (DEVICE_BEST → desktop popup), which opened
2180
+ // and closed without completing. Forcing REDIRECT here makes the click do
2181
+ // a full-page navigation to Klarna's hosted journey, returning via
2182
+ // `returnUrl` (recovered by resumeKlarnaReturn()).
2183
+ initiationMode: "REDIRECT"
2184
+ });
2185
+ const returnUrl = opts.returnUrl ?? (typeof window !== "undefined" ? window.location.origin + window.location.pathname : "");
2186
+ let settled = false;
2187
+ button.on("click", () => {
2188
+ if (opts.pendingKey && opts.pendingContext && typeof sessionStorage !== "undefined") {
2189
+ try {
2190
+ sessionStorage.setItem(opts.pendingKey, JSON.stringify(opts.pendingContext));
2191
+ } catch {
2192
+ }
2193
+ }
2194
+ const promise = sdk.Payment.initiate(() => ({
2195
+ currency: opts.paymentRequest.currency,
2196
+ paymentAmount: opts.paymentRequest.amount,
2197
+ intents: ["PAY"],
2198
+ locale: getGlobalLocale(),
2199
+ // REDIRECT = full-page navigation to Klarna's hosted journey, then back
2200
+ // to `returnUrl` with the result. We force this (vs DEVICE_BEST's desktop
2201
+ // popup) because the popup proved unreliable here — it opened and closed
2202
+ // without completing. The redirect flow has no popup-blocker / window-
2203
+ // messaging failure modes; the return is recovered via resumeKlarnaReturn().
2204
+ initiationMode: "REDIRECT",
2205
+ returnUrl,
2206
+ ...opts.paymentRequest.supplementaryPurchaseData ? { supplementaryPurchaseData: opts.paymentRequest.supplementaryPurchaseData } : {}
2207
+ }));
2208
+ Promise.resolve(promise).then((result) => {
2209
+ if (settled) return;
2210
+ const token = findAuthToken(result);
2211
+ if (token) {
2212
+ settled = true;
2213
+ opts.onAuthorize({ authorizationToken: token, raw: result });
2214
+ } else if (typeof console !== "undefined") {
2215
+ console.warn("[VioKlarna] authorize resolved without a token:", result);
2216
+ }
2217
+ }).catch((err) => {
2218
+ opts.onError?.(err);
2219
+ });
2220
+ return promise;
2221
+ });
2222
+ button.mount(opts.container);
2223
+ return {
2224
+ unmount: () => {
2225
+ try {
2226
+ button.unmount();
2227
+ } catch {
2228
+ }
2229
+ }
2230
+ };
2231
+ }
2232
+ function readKlarnaTokenFromUrl() {
2233
+ if (typeof window === "undefined") return null;
2234
+ const params = new URLSearchParams(window.location.search);
2235
+ for (const name of [
2236
+ "authorization_token",
2237
+ "authorizationToken",
2238
+ "klarna_authorization_token",
2239
+ "vio_klarna_token"
2240
+ ]) {
2241
+ const v = params.get(name);
2242
+ if (v && v.length > 0 && v !== "{klarna.authorization_token}") {
2243
+ return { token: v, param: name };
2244
+ }
2245
+ }
2246
+ return null;
2247
+ }
2248
+ function cleanUrlParams(params) {
2249
+ if (typeof window === "undefined" || !window.history?.replaceState) return;
2250
+ const url = new URL(window.location.href);
2251
+ let changed = false;
2252
+ for (const p of params) {
2253
+ if (url.searchParams.has(p)) {
2254
+ url.searchParams.delete(p);
2255
+ changed = true;
2256
+ }
2257
+ }
2258
+ if (changed) window.history.replaceState({}, "", url.toString());
2259
+ }
2260
+ async function listenForKlarnaCompletion(opts) {
2261
+ let sdk;
2262
+ try {
2263
+ sdk = await loadKlarnaSdk(opts.config);
2264
+ } catch (err) {
2265
+ if (typeof console !== "undefined") {
2266
+ console.warn("[VioKlarna] resume: SDK load failed:", err);
2267
+ }
2268
+ return;
2269
+ }
2270
+ let settled = false;
2271
+ const onComplete = (...args) => {
2272
+ if (settled) return;
2273
+ for (const a of args) {
2274
+ const token = findAuthToken(a);
2275
+ if (token) {
2276
+ settled = true;
2277
+ opts.onComplete({ authorizationToken: token, raw: a });
2278
+ return;
2279
+ }
2280
+ }
2281
+ };
2282
+ try {
2283
+ sdk.Payment.on?.("complete", onComplete);
2284
+ } catch {
2285
+ }
2286
+ try {
2287
+ sdk.Payment.on?.("error", (...a) => opts.onError?.(a[0]));
2288
+ } catch {
2289
+ }
2290
+ }
2291
+
2292
+ // src/core/checkout/payments/klarna-payments.ts
2293
+ var KLARNA_KP_LIB_URL = "https://x.klarnacdn.net/kp/lib/v1/api.js";
2294
+ var kpLoad = null;
2295
+ function loadKpLib() {
2296
+ if (typeof window === "undefined" || typeof document === "undefined") {
2297
+ return Promise.reject(new Error("[VioKlarnaKP] requires a browser environment"));
2298
+ }
2299
+ if (window.Klarna?.Payments) return Promise.resolve(window.Klarna);
2300
+ if (kpLoad) return kpLoad;
2301
+ kpLoad = new Promise((resolve, reject) => {
2302
+ const timer = setTimeout(
2303
+ () => reject(new Error("[VioKlarnaKP] kp/lib/v1/api.js load timed out")),
2304
+ 12e3
2305
+ );
2306
+ const ready = () => {
2307
+ clearTimeout(timer);
2308
+ if (window.Klarna?.Payments) resolve(window.Klarna);
2309
+ else reject(new Error("[VioKlarnaKP] Klarna global missing after ready"));
2310
+ };
2311
+ const prev = window.klarnaAsyncCallback;
2312
+ window.klarnaAsyncCallback = () => {
2313
+ if (typeof prev === "function") {
2314
+ try {
2315
+ prev();
2316
+ } catch {
2317
+ }
2318
+ }
2319
+ ready();
2320
+ };
2321
+ const existing = document.querySelector(
2322
+ `script[src="${KLARNA_KP_LIB_URL}"]`
2323
+ );
2324
+ if (existing) {
2325
+ if (window.Klarna?.Payments) ready();
2326
+ } else {
2327
+ const s = document.createElement("script");
2328
+ s.src = KLARNA_KP_LIB_URL;
2329
+ s.async = true;
2330
+ s.onerror = () => {
2331
+ clearTimeout(timer);
2332
+ kpLoad = null;
2333
+ reject(new Error("[VioKlarnaKP] failed to load kp/lib/v1/api.js"));
2334
+ };
2335
+ document.head.appendChild(s);
2336
+ }
2337
+ }).catch((err) => {
2338
+ kpLoad = null;
2339
+ throw err;
2340
+ });
2341
+ return kpLoad;
2342
+ }
2343
+ async function isKlarnaPaymentsAvailable() {
2344
+ if (typeof window === "undefined") return false;
2345
+ try {
2346
+ await loadKpLib();
2347
+ return true;
2348
+ } catch (err) {
2349
+ if (typeof console !== "undefined") {
2350
+ console.warn("[VioKlarnaKP] availability check failed:", err);
2351
+ }
2352
+ return false;
2353
+ }
2354
+ }
2355
+ function findPayNowCategory(categories) {
2356
+ if (!Array.isArray(categories) || categories.length === 0) return null;
2357
+ const byId = (re) => categories.find((c) => c?.identifier && re.test(c.identifier));
2358
+ const byName = (re) => categories.find((c) => c?.name && re.test(c.name));
2359
+ const cat = categories.find((c) => c?.identifier === "pay_now") ?? byName(/betal\s*n[åa]|pay\s*now/i) ?? byId(/pay_now|direct_debit|direct_bank_transfer|pay_in_full|klarna_pay_now/i) ?? byName(/direkte|bank|kort|straks|sofort/i) ?? categories.find((c) => c?.identifier && !/pay_later|pay_over_time|slice_it/.test(c.identifier));
2360
+ return cat?.identifier ?? null;
2361
+ }
2362
+ async function createKlarnaPaymentsWidget(input) {
2363
+ const Klarna = await loadKpLib();
2364
+ Klarna.Payments.init({ client_token: input.clientToken });
2365
+ let selected = input.category ?? findPayNowCategory(input.categories) ?? input.categories[0]?.identifier ?? "pay_now";
2366
+ return {
2367
+ categories: input.categories,
2368
+ get selected() {
2369
+ return selected;
2370
+ },
2371
+ load(category) {
2372
+ if (category) selected = category;
2373
+ return new Promise((resolve, reject) => {
2374
+ try {
2375
+ Klarna.Payments.load(
2376
+ { container: input.container, payment_method_category: selected },
2377
+ {},
2378
+ (res) => {
2379
+ if (res?.error) reject(res.error);
2380
+ else resolve({ showForm: res?.show_form !== false });
2381
+ }
2382
+ );
2383
+ } catch (err) {
2384
+ reject(err);
2385
+ }
2386
+ });
2387
+ },
2388
+ authorize(data) {
2389
+ return new Promise((resolve, reject) => {
2390
+ try {
2391
+ Klarna.Payments.authorize(
2392
+ { payment_method_category: selected, auto_finalize: true },
2393
+ data ?? {},
2394
+ (res) => {
2395
+ if (res?.approved && res.authorization_token) {
2396
+ resolve(res.authorization_token);
2397
+ } else if (res?.error) {
2398
+ reject(res.error);
2399
+ } else if (res?.finalize_required) {
2400
+ reject(new Error("Klarna requires a finalize step (multistep)"));
2401
+ } else {
2402
+ reject(new Error("Klarna authorization was not approved"));
2403
+ }
2404
+ }
2405
+ );
2406
+ } catch (err) {
2407
+ reject(err);
2408
+ }
2409
+ });
2410
+ }
2411
+ };
2412
+ }
2413
+
2414
+ // src/core/checkout/checkout-manager.ts
2415
+ function describeError(err) {
2416
+ if (err instanceof Error) return err.message;
2417
+ if (typeof err === "string") return err;
2418
+ if (err && typeof err === "object") {
2419
+ const o = err;
2420
+ for (const k of ["message", "error", "reason", "description"]) {
2421
+ if (typeof o[k] === "string" && o[k]) return o[k];
2422
+ }
2423
+ try {
2424
+ return JSON.stringify(err);
2425
+ } catch {
2426
+ return String(err);
2427
+ }
2428
+ }
2429
+ return String(err);
2430
+ }
2431
+ var KLARNA_PENDING_KEY = "vio.klarna.pending.v1";
2432
+ var KLARNA_SHIPPING_OPTIONS = [
2433
+ {
2434
+ id: "standard",
2435
+ method: "Standard",
2436
+ description: "3\u20135 virkedager",
2437
+ price: 4900,
2438
+ tax_amount: 0,
2439
+ tax_rate: 0,
2440
+ preselected: true
2441
+ },
2442
+ {
2443
+ id: "express",
2444
+ method: "Express",
2445
+ description: "1\u20132 virkedager",
2446
+ price: 9900,
2447
+ tax_amount: 0,
2448
+ tax_rate: 0,
2449
+ preselected: false
2450
+ }
2451
+ ];
2452
+ var CREATE_PAYMENT_KLARNA_NATIVE_MUTATION = `
2453
+ mutation CreatePaymentKlarnaNative($checkoutId: String!, $countryCode: String, $currency: String, $locale: String, $returnUrl: String, $intent: String, $customer: KlarnaNativeCustomerInput, $billingAddress: KlarnaNativeAddressInput, $shippingAddress: KlarnaNativeAddressInput, $autoCapture: Boolean) {
2454
+ Payment {
2455
+ CreatePaymentKlarnaNative(checkout_id: $checkoutId, country_code: $countryCode, currency: $currency, locale: $locale, return_url: $returnUrl, intent: $intent, customer: $customer, billing_address: $billingAddress, shipping_address: $shippingAddress, auto_capture: $autoCapture) {
2456
+ client_token
2457
+ session_id
2458
+ purchase_country
2459
+ purchase_currency
2460
+ cart_id
2461
+ checkout_id
2462
+ payment_method_categories {
2463
+ identifier
2464
+ name
2465
+ asset_urls {
2466
+ descriptive
2467
+ standard
2468
+ }
2469
+ }
2470
+ }
2471
+ }
2472
+ }
2473
+ `;
2474
+ var CONFIRM_PAYMENT_KLARNA_NATIVE_MUTATION = `
2475
+ mutation ConfirmPaymentKlarnaNative($checkoutId: String!, $authorizationToken: String!, $autoCapture: Boolean, $billingAddress: KlarnaNativeAddressInput, $shippingAddress: KlarnaNativeAddressInput, $customer: KlarnaNativeCustomerInput) {
2476
+ Payment {
2477
+ ConfirmPaymentKlarnaNative(checkout_id: $checkoutId, authorization_token: $authorizationToken, auto_capture: $autoCapture, billing_address: $billingAddress, shipping_address: $shippingAddress, customer: $customer) {
2478
+ order_id
2479
+ checkout_id
2480
+ fraud_status
2481
+ order {
2482
+ order_id
2483
+ status
2484
+ purchase_country
2485
+ purchase_currency
2486
+ locale
2487
+ billing_address {
2488
+ given_name
2489
+ family_name
2490
+ email
2491
+ street_address
2492
+ postal_code
2493
+ city
2494
+ country
2495
+ }
2496
+ shipping_address {
2497
+ given_name
2498
+ family_name
2499
+ email
2500
+ street_address
2501
+ postal_code
2502
+ city
2503
+ country
2504
+ }
2505
+ order_amount
2506
+ order_tax_amount
2507
+ total_line_items_price
2508
+ order_lines {
2509
+ type
2510
+ name
2511
+ quantity
2512
+ unit_price
2513
+ tax_rate
2514
+ total_amount
2515
+ total_discount_amount
2516
+ total_tax_amount
2517
+ merchant_data
2518
+ }
2519
+ merchant_urls {
2520
+ terms
2521
+ checkout
2522
+ confirmation
2523
+ push
2524
+ }
2525
+ html_snippet
2526
+ started_at
2527
+ last_modified_at
2528
+ options {
2529
+ allow_separate_shipping_address
2530
+ date_of_birth_mandatory
2531
+ require_validate_callback_success
2532
+ phone_mandatory
2533
+ auto_capture
2534
+ }
2535
+ shipping_options {
2536
+ id
2537
+ name
2538
+ price
2539
+ tax_amount
2540
+ tax_rate
2541
+ preselected
2542
+ }
2543
+ merchant_data
2544
+ selected_shipping_option {
2545
+ id
2546
+ name
2547
+ price
2548
+ tax_amount
2549
+ tax_rate
2550
+ preselected
2551
+ }
2552
+ }
2553
+ }
2554
+ }
2555
+ }
2556
+ `;
2557
+ function buildKlarnaCustomer(address) {
2558
+ if (!address) return null;
2559
+ return {
2560
+ dob: address.dob || address.date_of_birth || null,
2561
+ email: address.email || null,
2562
+ organization_registration_id: address.organization_registration_id || address.org_id || null,
2563
+ phone: address.phone || address.telephone || null,
2564
+ type: address.type || "person"
2565
+ };
2566
+ }
2567
+ function buildKlarnaAddress(address) {
2568
+ if (!address) return null;
2569
+ return {
2570
+ city: address.city || null,
2571
+ country: address.country || address.countryCode || getGlobalCountryCode(),
2572
+ email: address.email || null,
2573
+ family_name: address.familyName || address.family_name || address.last_name || address.lastName || null,
2574
+ given_name: address.givenName || address.given_name || address.first_name || address.firstName || null,
2575
+ phone: address.phone || null,
2576
+ postal_code: address.postalCode || address.postal_code || address.zip || null,
2577
+ region: address.region || address.state || null,
2578
+ street_address: address.streetAddress || address.street_address || address.address1 || address.street || address.address || // CheckoutAddress uses plain `address` for the street
2579
+ null
2580
+ // street_address2 intentionally omitted — the KlarnaNativeAddressInput
2581
+ // backend type rejects it.
2582
+ };
2583
+ }
2584
+ function buildCheckoutAddress(address) {
2585
+ if (!address) return null;
2586
+ const firstName = address.firstName || address.first_name || address.givenName || address.given_name || null;
2587
+ const lastName = address.lastName || address.last_name || address.familyName || address.family_name || null;
2588
+ const address1 = address.address || address.address1 || address.street || address.street_address || null;
2589
+ const city = address.city || null;
2590
+ const zip = address.postalCode || address.postal_code || address.zip || null;
2591
+ const country_code = address.countryCode || address.country_code || address.country || getGlobalCountryCode();
2592
+ if (!firstName && !lastName && !address1 && !city && !zip) return null;
2593
+ return {
2594
+ first_name: firstName,
2595
+ last_name: lastName,
2596
+ address1,
2597
+ city,
2598
+ zip,
2599
+ country_code
2600
+ };
2601
+ }
2602
+ function resolveHttpsReturnUrl(overrideUrl, params = null) {
2603
+ let url = void 0;
2604
+ if (!url && typeof window !== "undefined" && window.location) {
2605
+ url = window.location.href;
2606
+ }
2607
+ if (!url || typeof url !== "string" || url.startsWith("about:") || url.startsWith("file:")) {
2608
+ url = "https://vev.design/checkout/confirmation";
2609
+ }
2610
+ if (url.startsWith("http://")) {
2611
+ url = "https://" + url.slice(7);
2612
+ } else if (!url.startsWith("https://")) {
2613
+ url = "https://" + url.replace(/^[a-zA-Z]+:\/\//, "");
2614
+ }
2615
+ if (params && typeof params === "object") {
2616
+ try {
2617
+ const parsed = new URL(url);
2618
+ Object.keys(params).forEach((k) => {
2619
+ if (params[k] !== void 0 && params[k] !== null) {
2620
+ parsed.searchParams.set(k, String(params[k]));
2621
+ }
2622
+ });
2623
+ return parsed.toString();
2624
+ } catch {
2625
+ const queryStr = Object.keys(params).filter((k) => params[k] !== void 0 && params[k] !== null).map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(String(params[k]))}`).join("&");
2626
+ if (queryStr) {
2627
+ url += (url.includes("?") ? "&" : "?") + queryStr;
2628
+ }
2629
+ }
2630
+ }
2631
+ return url;
2632
+ }
2633
+ async function executeKlarnaGraphQL(query, variables, sponsorId) {
2634
+ return executeCartGraphQL(query, variables, await getCartGraphQLOptions(sponsorId));
2635
+ }
2636
+ var CheckoutManager = class _CheckoutManager extends EventTarget {
2637
+ constructor(cartManager) {
2638
+ super();
2639
+ this.cartManager = cartManager;
2640
+ this.state = null;
2641
+ this.applePayAvailableCache = null;
2642
+ /** Gateway hints learned from CreatePaymentApplePay (per session). The
2643
+ * backend's gateway_merchant_id may be a publishable key (pk_…) or a Stripe
2644
+ * Connect account (acct_…) — stored defensively by prefix. */
2645
+ this.applePayPublishableKey = null;
2646
+ this.applePayConnectedAccount = null;
2647
+ this.klarnaAvailableCache = null;
2648
+ this.klarnaOrderInFlight = false;
2649
+ /** Last backend shippings fetched (per-supplier), for UI reuse. */
2650
+ this.lastFetchedShippings = [];
2651
+ /** Per-sponsor payment methods cache + in-flight dedupe. */
2652
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2653
+ this.paymentMethodsCache = /* @__PURE__ */ new Map();
2654
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2655
+ this.paymentMethodsPromises = /* @__PURE__ */ new Map();
2656
+ preloadStripeJs();
2657
+ }
2658
+ open(sponsorId) {
2659
+ const cart = this.cartManager.getCart(sponsorId);
2660
+ if (!cart || cart.items.length === 0) {
2661
+ throw new Error(`[VioCheckout] sponsor ${sponsorId} has no cart items`);
2662
+ }
2663
+ this.state = {
2664
+ sponsorId,
2665
+ subtotal: this.cartManager.subtotalForSponsor(sponsorId),
2666
+ // Page-level currency wins over whatever the persisted cart carries —
2667
+ // the host may have switched currency since the cart was created.
2668
+ currency: getGlobalCurrency()
2669
+ };
2670
+ this.emit();
2671
+ return this.state;
2672
+ }
2673
+ close() {
2674
+ this.state = null;
2675
+ this.applePayAvailableCache = null;
2676
+ this.klarnaAvailableCache = null;
2677
+ this.emit();
2678
+ }
2679
+ // MARK: - Backend checkout (Vio Commerce GraphQL)
2680
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2681
+ /** Create the backend checkout for the sponsor's cart (auto-accepts terms). */
2682
+ async createCheckout(sponsorId) {
2683
+ const cart = this.cartManager.getCart(sponsorId);
2684
+ let cartId = cart?.cartId;
2685
+ if (!cartId) {
2686
+ cartId = await this.cartManager.ensureCartId(sponsorId, cart?.currency);
2687
+ if (!cartId) {
2688
+ throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${sponsorId}`);
2689
+ }
2690
+ }
2691
+ const opts = await getCartGraphQLOptions(sponsorId);
2692
+ const checkout = await createCheckout(cartId, opts);
2693
+ if (checkout?.id) {
2694
+ this.state = {
2695
+ ...this.state ?? { sponsorId, subtotal: 0, currency: getGlobalCurrency() },
2696
+ sponsorId,
2697
+ checkoutId: checkout.id,
2698
+ checkout
2699
+ };
2700
+ this.emit();
2701
+ try {
2702
+ const updated = await updateCheckout(
2703
+ {
2704
+ checkoutId: checkout.id,
2705
+ buyerAcceptsPurchaseConditions: true,
2706
+ buyerAcceptsTermsConditions: true
2707
+ },
2708
+ opts
2709
+ );
2710
+ if (updated?.id) {
2711
+ this.state = { ...this.state, checkout: updated };
2712
+ this.emit();
2713
+ return updated;
2714
+ }
2715
+ } catch (err) {
2716
+ if (typeof console !== "undefined") {
2717
+ console.warn("[CheckoutManager] Failed to auto-accept terms & purchase conditions:", err);
2718
+ }
2719
+ }
2720
+ }
2721
+ return checkout;
2722
+ }
2723
+ /** Update the backend checkout (terms are re-asserted unless overridden). */
2724
+ async updateCheckout(variables, sponsorId) {
2725
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
2726
+ const fullVars = {
2727
+ buyerAcceptsPurchaseConditions: true,
2728
+ buyerAcceptsTermsConditions: true,
2729
+ ...variables
2730
+ };
2731
+ const updated = await updateCheckout(fullVars, opts);
2732
+ if (updated?.id && this.state) {
2733
+ this.state = { ...this.state, checkoutId: updated.id, checkout: updated };
2734
+ this.emit();
2735
+ }
2736
+ return updated;
2737
+ }
2738
+ async getCheckout(checkoutId, sponsorId) {
2739
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
2740
+ return getCheckout(checkoutId, opts);
2741
+ }
2742
+ /**
2743
+ * Payment methods enabled for the sponsor (backend-configured). Memoised per
2744
+ * sponsor with in-flight dedupe — several UI components ask on every open.
2745
+ */
2746
+ async getAvailablePaymentMethods(sponsorId) {
2747
+ const spId = sponsorId ?? this.state?.sponsorId;
2748
+ if (!spId) return null;
2749
+ if (this.paymentMethodsCache.has(spId)) {
2750
+ return this.paymentMethodsCache.get(spId);
2751
+ }
2752
+ const inFlight = this.paymentMethodsPromises.get(spId);
2753
+ if (inFlight) return inFlight;
2754
+ const promise = (async () => {
2755
+ try {
2756
+ const opts = await getCartGraphQLOptions(spId);
2757
+ const res = await getAvailablePaymentMethods(opts);
2758
+ if (res) this.paymentMethodsCache.set(spId, res);
2759
+ return res;
2760
+ } finally {
2761
+ this.paymentMethodsPromises.delete(spId);
2762
+ }
2763
+ })();
2764
+ this.paymentMethodsPromises.set(spId, promise);
2765
+ return promise;
2766
+ }
2767
+ clearPaymentMethodsCache() {
2768
+ this.paymentMethodsCache.clear();
2769
+ this.paymentMethodsPromises.clear();
2770
+ }
2771
+ /**
2772
+ * Real shipping options from the backend cart (per supplier), mapped to the
2773
+ * KlarnaShippingOption shape (minor units). Empty array on failure — callers
2774
+ * fall back to the static KLARNA_SHIPPING_OPTIONS.
2775
+ */
2776
+ async fetchAvailableShippings(sponsorId) {
2777
+ try {
2778
+ const spId = sponsorId ?? this.state?.sponsorId;
2779
+ if (!spId) return [];
2780
+ const cart = this.cartManager.getCart(spId);
2781
+ let cartId = cart?.cartId;
2782
+ if (!cartId) {
2783
+ cartId = await this.cartManager.ensureCartId(spId);
2784
+ }
2785
+ if (!cartId) return [];
2786
+ const opts = await getCartGraphQLOptions(spId);
2787
+ const supplierGroups = await getLineItemsBySupplier(cartId, opts);
2788
+ const firstGroup = Array.isArray(supplierGroups) ? supplierGroups[0] : null;
2789
+ const shippings = firstGroup?.available_shippings ?? [];
2790
+ if (Array.isArray(shippings) && shippings.length > 0) {
2791
+ const supplierId = firstGroup?.supplier?.id;
2792
+ const mapped = shippings.map((s, idx) => {
2793
+ const priceObj = s.price ?? {};
2794
+ const amount = typeof priceObj.amount_incl_taxes === "number" ? priceObj.amount_incl_taxes : typeof priceObj.amount === "number" ? priceObj.amount : 0;
2795
+ return {
2796
+ id: String(s.id),
2797
+ supplierId: supplierId != null ? String(supplierId) : void 0,
2798
+ method: s.name || "Default Shipping",
2799
+ name: s.name || "Default Shipping",
2800
+ description: s.description || "",
2801
+ country_code: s.country_code || "NO",
2802
+ price: Math.round(amount * 100),
2803
+ priceMajor: amount,
2804
+ currency: priceObj.currency_code || getGlobalCurrency(),
2805
+ tax_amount: Math.round((priceObj.tax_amount ?? 0) * 100),
2806
+ tax_rate: Math.round((priceObj.tax_rate ?? 0) * 100),
2807
+ preselected: idx === 0
2808
+ };
2809
+ });
2810
+ this.lastFetchedShippings = mapped;
2811
+ return mapped;
2812
+ }
2813
+ } catch (err) {
2814
+ if (typeof console !== "undefined") {
2815
+ console.warn("[CheckoutManager] fetchAvailableShippings failed:", err);
2816
+ }
2817
+ }
2818
+ return [];
2819
+ }
2820
+ /** Set the chosen shipping per supplier on the backend cart. */
2821
+ async updateShippingsBySupplier(data, sponsorId) {
2822
+ const spId = sponsorId ?? this.state?.sponsorId;
2823
+ if (!spId) return null;
2824
+ const cart = this.cartManager.getCart(spId);
2825
+ if (!cart?.cartId) return null;
2826
+ const opts = await getCartGraphQLOptions(spId);
2827
+ const updated = await updateShippingsBySupplier(cart.cartId, data, opts);
2828
+ if (updated) {
2829
+ this.cartManager.updateFromBackendCart(spId, updated);
2830
+ }
2831
+ return updated;
2832
+ }
2833
+ /**
2834
+ * Shared setup for the redirect payment links (Stripe / Vipps): ensure the
2835
+ * backend cart + checkout exist, push email/address onto the checkout, and
2836
+ * return everything the payment mutation needs.
2837
+ */
2838
+ async prepareRedirectPayment(sponsorId, formData, extraUpdateVars = {}) {
2839
+ const spId = sponsorId ?? this.state?.sponsorId;
2840
+ if (!spId) throw new Error("[CheckoutManager] no sponsor for payment");
2841
+ const cart = this.cartManager.getCart(spId);
2842
+ let cartId = cart?.cartId;
2843
+ if (!cartId) {
2844
+ cartId = await this.cartManager.ensureCartId(spId);
2845
+ }
2846
+ if (!cartId) {
2847
+ throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
2848
+ }
2849
+ const opts = await getCartGraphQLOptions(spId);
2850
+ let checkoutId = this.state?.checkoutId;
2851
+ if (!checkoutId) {
2852
+ const checkoutRes = await createCheckout(cartId, opts);
2853
+ checkoutId = checkoutRes?.id;
2854
+ }
2855
+ if (!checkoutId) {
2856
+ throw new Error("[CheckoutManager] no backend checkout \u2014 cannot start the payment");
2857
+ }
2858
+ const isVipps = extraUpdateVars?.paymentMethod === "Vipps";
2859
+ const addr = typeof formData === "object" && formData !== null ? formData : this.state?.address;
2860
+ const addressObj = !isVipps ? buildCheckoutAddress(addr) : null;
2861
+ const emailVal = (typeof formData === "string" ? formData : formData?.email) || this.state?.address?.email || "";
2862
+ if (!emailVal || !emailVal.includes("@")) {
2863
+ throw new Error("[CheckoutManager] email is required to start the payment");
2864
+ }
2865
+ const updateVars = {
2866
+ checkoutId,
2867
+ email: emailVal,
2868
+ buyerAcceptsPurchaseConditions: true,
2869
+ buyerAcceptsTermsConditions: true,
2870
+ ...extraUpdateVars
2871
+ };
2872
+ if (addressObj) {
2873
+ updateVars.shippingAddress = addressObj;
2874
+ updateVars.billingAddress = addressObj;
2875
+ }
2876
+ try {
2877
+ const updated = await updateCheckout(updateVars, opts);
2878
+ if (updated?.id) {
2879
+ this.state = {
2880
+ ...this.state ?? { sponsorId: spId, subtotal: 0, currency: getGlobalCurrency() },
2881
+ sponsorId: spId,
2882
+ checkoutId: updated.id,
2883
+ checkout: updated
2884
+ };
2885
+ this.emit();
2886
+ checkoutId = updated.id;
2887
+ }
2888
+ } catch (err) {
2889
+ throw new Error(
2890
+ `[CheckoutManager] could not persist checkout details: ${describeError(err)}`
2891
+ );
2892
+ }
2893
+ if (!checkoutId) {
2894
+ throw new Error("[CheckoutManager] no backend checkout \u2014 cannot start the payment");
2895
+ }
2896
+ return { spId, checkoutId, emailVal, opts };
2897
+ }
2898
+ /** Redirect the top window to a payment provider URL. */
2899
+ static redirectTo(url) {
2900
+ if (typeof window === "undefined") return;
2901
+ try {
2902
+ if (window.top && window.top !== window) {
2903
+ window.top.location.href = url;
2904
+ } else {
2905
+ window.location.href = url;
2906
+ }
2907
+ } catch {
2908
+ window.open(url, "_top") || window.open(url, "_blank");
2909
+ }
2910
+ }
2911
+ /**
2912
+ * Stripe payment link: create/refresh the backend checkout, mint the Stripe
2913
+ * Checkout URL (CreatePaymentStripe) and redirect. The return URL carries
2914
+ * vio_payment/vio_method/checkout_id so the page can show the confirmation.
2915
+ */
2916
+ async startStripePayment(sponsorId, formData) {
2917
+ const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
2918
+ sponsorId,
2919
+ formData,
2920
+ { paymentMethod: "Stripe" }
2921
+ );
2922
+ const successUrl = resolveHttpsReturnUrl(null, {
2923
+ vio_payment: "success",
2924
+ vio_method: "stripe",
2925
+ vio_sponsor: spId,
2926
+ checkout_id: checkoutId
2927
+ });
2928
+ const stripeRes = await createPaymentStripe(
2929
+ { checkoutId, successUrl, paymentMethod: "Stripe", email: emailVal },
2930
+ opts
2931
+ );
2932
+ if (stripeRes?.checkout_url) {
2933
+ _CheckoutManager.redirectTo(stripeRes.checkout_url);
2934
+ return stripeRes;
2935
+ }
2936
+ throw new Error("Stripe payment link generation failed: missing checkout_url");
2937
+ }
2938
+ /**
2939
+ * Vipps payment link: same flow as Stripe — CreatePaymentVipps → redirect to
2940
+ * payment_url.
2941
+ */
2942
+ async startVippsPayment(sponsorId, formData) {
2943
+ const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
2944
+ sponsorId,
2945
+ formData,
2946
+ { paymentMethod: "Vipps" }
2947
+ );
2948
+ const returnUrl = resolveHttpsReturnUrl(null, {
2949
+ vio_payment: "success",
2950
+ vio_method: "vipps",
2951
+ vio_sponsor: spId,
2952
+ checkout_id: checkoutId
2953
+ });
2954
+ const vippsRes = await createPaymentVipps(
2955
+ { checkoutId, email: emailVal, returnUrl },
2956
+ opts
2957
+ );
2958
+ if (vippsRes?.payment_url) {
2959
+ _CheckoutManager.redirectTo(vippsRes.payment_url);
2960
+ return vippsRes;
2961
+ }
2962
+ throw new Error("Vipps payment link generation failed: missing payment_url");
2963
+ }
2964
+ /** Vipps' own payment state for the checkout — see getVippsStatus in
2965
+ * cart-queries.ts for why this exists instead of trusting GetCheckout. */
2966
+ async getVippsStatus(checkoutId, sponsorId) {
2967
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
2968
+ return getVippsStatus({ checkoutId }, opts);
2969
+ }
2970
+ /* eslint-enable @typescript-eslint/no-explicit-any */
2971
+ setAddress(address) {
2972
+ if (!this.state) return;
2973
+ this.state = { ...this.state, address };
2974
+ this.emit();
2975
+ }
2976
+ selectPaymentMethod(method) {
2977
+ if (!this.state) return;
2978
+ this.state = { ...this.state, paymentMethod: method };
2979
+ this.emit();
2980
+ this.dispatchEvent(
2981
+ new CustomEvent("payment-select", {
2982
+ detail: { method, state: this.state }
2983
+ })
2984
+ );
2985
+ }
2986
+ // MARK: - Apple Pay (Stripe Payment Request)
2987
+ /**
2988
+ * Returns true if Apple Pay can be shown on this device + Stripe config.
2989
+ * Result is memoised — call `clearApplePayCache()` if config changes.
2990
+ */
2991
+ async isApplePayAvailable(country = "NO") {
2992
+ if (this.applePayAvailableCache !== null) return this.applePayAvailableCache;
2993
+ const cfg = Configuration.isInitialized ? Configuration.get() : null;
2994
+ const publishableKey = cfg?.stripePublishableKey ?? "";
2995
+ if (!publishableKey || !this.state) {
2996
+ this.applePayAvailableCache = false;
2997
+ return false;
2998
+ }
2999
+ const amount = toSmallestUnit(this.state.subtotal, this.state.currency);
3000
+ const available = await checkApplePayAvailability({
3001
+ publishableKey,
3002
+ connectedAccount: this.findStripeConnectAccount() ?? void 0,
3003
+ amount,
3004
+ currency: this.state.currency,
3005
+ country,
3006
+ label: "Vio"
3007
+ });
3008
+ this.applePayAvailableCache = available;
3009
+ return available;
3010
+ }
3011
+ clearApplePayCache() {
3012
+ this.applePayAvailableCache = null;
3013
+ }
3014
+ /**
3015
+ * Prepare Apple Pay for the product-page express button (no active checkout
3016
+ * needed). Pre-creates the Stripe PaymentRequest + resolves canMakePayment so
3017
+ * the returned `show()` can fire synchronously inside the tap (Apple blocks
3018
+ * the sheet if `show()` is reached after an await). `show()` resolves the
3019
+ * sheet, then dispatches `payment-complete` (→ confirmation) or
3020
+ * `payment-error`. `available` is true only when Apple Pay will actually work.
3021
+ */
3022
+ async prepareApplePayFor(amount, currency, country = "NO") {
3023
+ const cfg = Configuration.isInitialized ? Configuration.get() : null;
3024
+ const publishableKey = this.applePayPublishableKey || cfg?.stripePublishableKey || "";
3025
+ if (!publishableKey) return { available: false, show: async () => {
3026
+ } };
3027
+ const prepared = await prepareApplePay({
3028
+ publishableKey,
3029
+ connectedAccount: this.applePayConnectedAccount ?? this.findStripeConnectAccount() ?? void 0,
3030
+ amount: toSmallestUnit(amount, currency),
3031
+ currency,
3032
+ country,
3033
+ label: "Vio",
3034
+ // Native Apple Pay shipping — same Standard/Express options as Klarna.
3035
+ shippingOptions: KLARNA_SHIPPING_OPTIONS.map((o) => ({
3036
+ id: o.id,
3037
+ label: o.method,
3038
+ detail: o.description,
3039
+ amount: o.price
3040
+ })),
3041
+ // The real backend charge — sponsor resolved at authorize-time (the
3042
+ // product-detail button calls checkout.open() before show()).
3043
+ onAuthorize: (raw) => {
3044
+ const spId = this.state?.sponsorId ?? [...this.cartManager.getAllCarts().keys()][0];
3045
+ if (spId == null) {
3046
+ return Promise.reject(new Error("[VioCheckout] no sponsor for Apple Pay"));
3047
+ }
3048
+ return this.buildApplePayAuthorize(spId)(raw);
3049
+ }
3050
+ });
3051
+ return {
3052
+ available: prepared.available,
3053
+ show: async () => {
3054
+ try {
3055
+ const result = await prepared.show();
3056
+ this.selectPaymentMethod("apple-pay");
3057
+ const state = this.state ?? { sponsorId: 0, subtotal: amount, currency };
3058
+ this.dispatchEvent(
3059
+ new CustomEvent("payment-complete", {
3060
+ detail: { method: "apple-pay", state, result }
3061
+ })
3062
+ );
3063
+ } catch (err) {
3064
+ if (typeof console !== "undefined") {
3065
+ console.warn("[VioCheckout] Apple Pay express failed:", err);
3066
+ }
3067
+ this.dispatchEvent(
3068
+ new CustomEvent("payment-error", {
3069
+ detail: { method: "apple-pay", error: describeError(err) }
3070
+ })
3071
+ );
3072
+ }
3073
+ }
3074
+ };
3075
+ }
3076
+ /**
3077
+ * Prepare an Apple Pay express charge for an arbitrary amount (e.g. the whole
3078
+ * cart) with NO active checkout. Unlike `prepareApplePayFor`, the returned
3079
+ * `show()` RESOLVES with the result (or `null` on cancel/failure) and does
3080
+ * NOT dispatch the global `payment-complete` — so no checkout overlay appears.
3081
+ * The caller (the cart drawer) renders its own confirmation. `show()` must be
3082
+ * called synchronously inside the tap (Apple's user-gesture rule).
3083
+ */
3084
+ async prepareApplePayExpress(amount, currency, country = "NO", sponsorId) {
3085
+ const cfg = Configuration.isInitialized ? Configuration.get() : null;
3086
+ const publishableKey = this.applePayPublishableKey || cfg?.stripePublishableKey || "";
3087
+ if (!publishableKey) return { available: false, show: async () => null };
3088
+ const spId = sponsorId ?? this.state?.sponsorId ?? [...this.cartManager.getAllCarts().keys()][0];
3089
+ const prepared = await prepareApplePay({
3090
+ publishableKey,
3091
+ connectedAccount: this.applePayConnectedAccount ?? this.findStripeConnectAccount() ?? void 0,
3092
+ amount: toSmallestUnit(amount, currency),
3093
+ currency,
3094
+ country,
3095
+ label: "Vio",
3096
+ // Native Apple Pay shipping (same Standard/Express options as Klarna) —
3097
+ // selected inside the sheet, so it's not a separate checkout step.
3098
+ shippingOptions: KLARNA_SHIPPING_OPTIONS.map((o) => ({
3099
+ id: o.id,
3100
+ label: o.method,
3101
+ detail: o.description,
3102
+ amount: o.price
3103
+ })),
3104
+ // The real backend charge, run inside the sheet's authorize event.
3105
+ ...spId != null ? { onAuthorize: this.buildApplePayAuthorize(spId) } : {}
3106
+ });
3107
+ return {
3108
+ available: prepared.available,
3109
+ show: async () => {
3110
+ try {
3111
+ return await prepared.show();
3112
+ } catch (err) {
3113
+ if (typeof console !== "undefined") {
3114
+ console.warn("[VioCheckout] cart Apple Pay express failed:", err);
3115
+ }
3116
+ return null;
3117
+ }
3118
+ }
3119
+ };
3120
+ }
3121
+ /** The charge closure run inside the Apple Pay sheet's authorize event —
3122
+ * shared by the checkout button and the cart's express button. Fail-closed:
3123
+ * any failure throws and the sheet closes with failure, nothing confirmed. */
3124
+ buildApplePayAuthorize(spId) {
3125
+ return async (raw) => {
3126
+ let checkoutId = this.state?.checkoutId;
3127
+ if (!checkoutId) {
3128
+ const created = await this.createCheckout(spId);
3129
+ checkoutId = created?.id ?? this.state?.checkoutId;
3130
+ }
3131
+ if (!checkoutId) {
3132
+ throw new Error("[VioCheckout] could not obtain a checkout for Apple Pay");
3133
+ }
3134
+ const email = raw.payerEmail || this.state?.address?.email || "";
3135
+ try {
3136
+ await this.updateCheckout(
3137
+ { checkoutId, paymentMethod: "Apple Pay", ...email ? { email } : {} },
3138
+ spId
3139
+ );
3140
+ } catch (err) {
3141
+ if (typeof console !== "undefined") {
3142
+ console.warn("[VioCheckout] updateCheckout before Apple Pay confirm failed:", err);
3143
+ }
3144
+ }
3145
+ const gqlOpts = await getCartGraphQLOptions(spId);
3146
+ const createRes = await createPaymentApplePay({ checkoutId }, gqlOpts);
3147
+ const gatewayId = String(createRes?.gateway_merchant_id ?? "");
3148
+ if (gatewayId.startsWith("pk_")) this.applePayPublishableKey = gatewayId;
3149
+ else if (gatewayId.startsWith("acct_")) this.applePayConnectedAccount = gatewayId;
3150
+ const addr = raw.shippingAddress ?? {};
3151
+ const str = (v) => typeof v === "string" ? v : "";
3152
+ const lines = Array.isArray(addr.addressLine) ? addr.addressLine : [];
3153
+ const fullName = str(addr.name) || raw.payerName || "";
3154
+ const shippingAddress = {
3155
+ address1: lines[0] || str(addr.line1) || "",
3156
+ address2: lines[1] || str(addr.line2) || "",
3157
+ city: str(addr.locality) || str(addr.city) || "",
3158
+ company: "",
3159
+ country: str(addr.country) || "",
3160
+ country_code: str(addr.countryCode) || getGlobalCountryCode(),
3161
+ first_name: str(addr.givenName) || fullName.split(" ")[0] || "",
3162
+ last_name: str(addr.familyName) || fullName.split(" ").slice(1).join(" ") || "",
3163
+ phone: str(addr.phone) || str(addr.phoneNumber) || raw.payerPhone || "",
3164
+ phone_code: "",
3165
+ province: str(addr.administrativeArea) || str(addr.state) || "",
3166
+ province_code: "",
3167
+ zip: str(addr.postalCode) || str(addr.postal_code) || ""
3168
+ };
3169
+ const confirmRes = await confirmPaymentApplePay(
3170
+ { checkoutId, applePayToken: raw.paymentMethodId, email, shippingAddress },
3171
+ gqlOpts
3172
+ );
3173
+ const status = String(confirmRes?.status ?? "").toLowerCase();
3174
+ if (!confirmRes || _CheckoutManager.APPLE_PAY_FAILED_STATUSES.has(status)) {
3175
+ throw new Error(
3176
+ `[VioCheckout] Apple Pay confirmation failed (status: ${confirmRes?.status ?? "none"})`
3177
+ );
3178
+ }
3179
+ return { orderId: confirmRes?.order_id, status: confirmRes?.status };
3180
+ };
3181
+ }
3182
+ static {
3183
+ /** Backend statuses from ConfirmPaymentApplePay that mean the charge did
3184
+ * NOT go through — anything here fails the sheet (fail-closed). */
3185
+ this.APPLE_PAY_FAILED_STATUSES = /* @__PURE__ */ new Set([
3186
+ "failed",
3187
+ "declined",
3188
+ "rejected",
3189
+ "cancelled",
3190
+ "canceled",
3191
+ "error",
3192
+ "expired"
3193
+ ]);
3194
+ }
3195
+ /**
3196
+ * Launches the Apple Pay sheet and charges server-side INSIDE the sheet's
3197
+ * authorize event (round-5 wiring, contract from the commerce backend):
3198
+ * 1. ensure a backend checkout exists (create if missing)
3199
+ * 2. UpdateCheckout(paymentMethod: "Apple Pay" + payer email)
3200
+ * 3. CreatePaymentApplePay → gateway hints (pk_/acct_ stored by prefix)
3201
+ * 4. ConfirmPaymentApplePay(token, email, shipping) → the REAL charge;
3202
+ * a failed/absent confirmation throws → the sheet closes with failure
3203
+ * Resolves null if the user closes the sheet. On success, dispatches
3204
+ * `payment-complete` with the backend order id in `result.confirmation`.
3205
+ */
3206
+ async startApplePay(options = {}) {
3207
+ if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
3208
+ const spId = options.sponsorId ?? this.state.sponsorId;
3209
+ const cfg = Configuration.isInitialized ? Configuration.get() : null;
3210
+ const publishableKey = this.applePayPublishableKey || cfg?.stripePublishableKey || "";
3211
+ if (!publishableKey) {
3212
+ throw new Error(
3213
+ "[VioCheckout] No Stripe publishable key configured. Pass `stripePublishableKey` to Vio.init({ ... })."
3214
+ );
3215
+ }
3216
+ const amount = toSmallestUnit(this.state.subtotal, this.state.currency);
3217
+ const onAuthorize = this.buildApplePayAuthorize(spId);
3218
+ const result = await showApplePaySheet({
3219
+ publishableKey,
3220
+ connectedAccount: this.applePayConnectedAccount ?? this.findStripeConnectAccount() ?? void 0,
3221
+ amount,
3222
+ currency: this.state.currency,
3223
+ country: options.country ?? "NO",
3224
+ label: options.label ?? "Vio",
3225
+ onAuthorize
3226
+ });
3227
+ if (!result) return null;
3228
+ this.selectPaymentMethod("apple-pay");
3229
+ this.dispatchEvent(
3230
+ new CustomEvent("payment-complete", {
3231
+ detail: { method: "apple-pay", state: this.state, result }
3232
+ })
3233
+ );
3234
+ return result;
3235
+ }
3236
+ // MARK: - Klarna Express Checkout (Klarna Web SDK v2)
3237
+ /**
3238
+ * True if the Klarna Express button can be offered (clientId configured +
3239
+ * SDK loads). Memoised per active checkout. Never throws.
3240
+ */
3241
+ async isKlarnaExpressAvailable() {
3242
+ if (this.klarnaAvailableCache !== null) return this.klarnaAvailableCache;
3243
+ const cfg = Configuration.isInitialized ? Configuration.get() : null;
3244
+ const clientId = cfg?.klarnaClientId ?? "";
3245
+ if (!clientId || !this.state) {
3246
+ this.klarnaAvailableCache = false;
3247
+ return false;
3248
+ }
3249
+ const available = await isKlarnaAvailable({
3250
+ clientId,
3251
+ locale: getGlobalLocale(),
3252
+ environment: cfg?.klarnaEnvironment
3253
+ });
3254
+ this.klarnaAvailableCache = available;
3255
+ return available;
3256
+ }
3257
+ clearKlarnaCache() {
3258
+ this.klarnaAvailableCache = null;
3259
+ }
3260
+ /**
3261
+ * Build the Klarna payment request from the active cart — amounts in the
3262
+ * currency's minor unit (øre/cents), one line item per cart line.
3263
+ */
3264
+ buildKlarnaPaymentRequest() {
3265
+ if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
3266
+ const currency = this.state.currency;
3267
+ const lineItems = this.items.map((item) => {
3268
+ const unitPrice = toSmallestUnit(item.unitPrice, currency);
3269
+ return {
3270
+ name: item.name || item.brand || "Produkt",
3271
+ quantity: item.quantity,
3272
+ unitPrice,
3273
+ totalAmount: unitPrice * item.quantity,
3274
+ reference: String(item.productId),
3275
+ ...item.imageUrl ? { imageUrl: item.imageUrl } : {}
3276
+ };
3277
+ });
3278
+ return {
3279
+ currency,
3280
+ amount: toSmallestUnit(this.state.subtotal, currency),
3281
+ supplementaryPurchaseData: { lineItems }
3282
+ };
3283
+ }
3284
+ /**
3285
+ * Mount the Klarna Express button into `container`. On a successful
3286
+ * authorization, fires `payment-complete` with the authorization token —
3287
+ * the host (or vio-backend) then creates the Klarna order server-side.
3288
+ *
3289
+ * Returns the button handle so the UI can unmount on teardown.
3290
+ */
3291
+ async mountKlarnaExpress(container) {
3292
+ if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
3293
+ const cfg = Configuration.get();
3294
+ if (!cfg.klarnaClientId) {
3295
+ throw new Error(
3296
+ "[VioCheckout] No Klarna client id configured. Pass `klarnaClientId` to Vio.init({ ... })."
3297
+ );
3298
+ }
3299
+ const pendingContext = this.buildKlarnaPendingContext();
3300
+ return mountKlarnaExpressButton({
3301
+ config: {
3302
+ clientId: cfg.klarnaClientId,
3303
+ locale: getGlobalLocale(),
3304
+ environment: cfg.klarnaEnvironment
3305
+ },
3306
+ container,
3307
+ paymentRequest: this.buildKlarnaPaymentRequest(),
3308
+ pendingKey: KLARNA_PENDING_KEY,
3309
+ pendingContext,
3310
+ // POPUP path: the initiate() promise resolves in-page with the token.
3311
+ onAuthorize: (result) => {
3312
+ void this.completeKlarnaOrder(result.authorizationToken, pendingContext, result);
3313
+ },
3314
+ onError: (err) => {
3315
+ if (typeof console !== "undefined") {
3316
+ console.warn("[VioCheckout] Klarna Express cancelled/error:", err);
3317
+ }
3318
+ this.dispatchEvent(
3319
+ new CustomEvent("payment-error", {
3320
+ detail: { method: "klarna", error: describeError(err) }
3321
+ })
3322
+ );
3323
+ }
3324
+ });
3325
+ }
3326
+ /**
3327
+ * Klarna Payments — classic widget flow (server client_token). Creates a
3328
+ * session on vio-backend, then mounts Klarna's inline widget into
3329
+ * `container`. Returns a handle to switch category and to authorize → which
3330
+ * on approval creates the order via the same backend /orders route as KEC.
3331
+ *
3332
+ * Unlike `mountKlarnaExpress`, this needs no public `klarnaClientId` and no
3333
+ * origin handshake — auth is the server-side API key.
3334
+ */
3335
+ async mountKlarnaPayments(container, opts) {
3336
+ if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
3337
+ const sponsorId = this.state.sponsorId;
3338
+ const fetchedShippings = await this.fetchAvailableShippings(sponsorId);
3339
+ const shippings = fetchedShippings.length > 0 ? fetchedShippings : KLARNA_SHIPPING_OPTIONS;
3340
+ const { ctx } = opts?.withShipping ? this.buildKlarnaInstantContext(opts.shippingId, shippings) : { ctx: this.buildKlarnaPendingContext() };
3341
+ const selectedOpt = shippings.find((s) => s.id === opts?.shippingId) ?? shippings[0];
3342
+ if (selectedOpt?.id && selectedOpt?.supplierId) {
3343
+ try {
3344
+ await this.updateShippingsBySupplier(
3345
+ [{ shipping_id: selectedOpt.id, supplier_id: Number(selectedOpt.supplierId) }],
3346
+ sponsorId
3347
+ );
3348
+ } catch (err) {
3349
+ if (typeof console !== "undefined") {
3350
+ console.warn("[CheckoutManager] updateShippingsBySupplier failed during mount:", err);
3351
+ }
3352
+ }
3353
+ }
3354
+ let checkoutId = ctx.checkoutId ?? this.state.checkoutId;
3355
+ try {
3356
+ const checkoutRes = await this.createCheckout(sponsorId);
3357
+ if (checkoutRes?.id) {
3358
+ checkoutId = checkoutRes.id;
3359
+ ctx.checkoutId = checkoutRes.id;
3360
+ }
3361
+ } catch (err) {
3362
+ if (typeof console !== "undefined") {
3363
+ console.warn("[CheckoutManager] CreateCheckout before Klarna init failed:", err);
3364
+ }
3365
+ }
3366
+ if (!checkoutId) {
3367
+ throw new Error("[CheckoutManager] no backend checkout for the Klarna session");
3368
+ }
3369
+ const address = this.state.address;
3370
+ const activeCurrency = getGlobalCurrency();
3371
+ if (this.state) this.state = { ...this.state, currency: activeCurrency };
3372
+ ctx.currency = activeCurrency;
3373
+ const variables = {
3374
+ checkoutId,
3375
+ countryCode: getGlobalCountryCode(),
3376
+ currency: activeCurrency,
3377
+ locale: getGlobalLocale(),
3378
+ returnUrl: resolveHttpsReturnUrl(null, {
3379
+ vio_payment: "success",
3380
+ vio_method: "klarna",
3381
+ vio_sponsor: sponsorId,
3382
+ checkout_id: checkoutId
3383
+ }),
3384
+ intent: "buy",
3385
+ customer: buildKlarnaCustomer(address),
3386
+ billingAddress: buildKlarnaAddress(address),
3387
+ shippingAddress: buildKlarnaAddress(address),
3388
+ autoCapture: true
3389
+ };
3390
+ const json = await executeKlarnaGraphQL(
3391
+ CREATE_PAYMENT_KLARNA_NATIVE_MUTATION,
3392
+ variables,
3393
+ sponsorId
3394
+ );
3395
+ const nativeRes = json?.data?.Payment?.CreatePaymentKlarnaNative;
3396
+ if (!nativeRes || !nativeRes.client_token) {
3397
+ throw new Error("Klarna session creation failed: missing client_token");
3398
+ }
3399
+ if (nativeRes.checkout_id) {
3400
+ ctx.checkoutId = nativeRes.checkout_id;
3401
+ this.state = { ...this.state, checkoutId: nativeRes.checkout_id };
3402
+ }
3403
+ if (nativeRes.session_id) {
3404
+ ctx.sessionId = nativeRes.session_id;
3405
+ this.state = { ...this.state, sessionId: nativeRes.session_id };
3406
+ }
3407
+ ctx.clientToken = nativeRes.client_token;
3408
+ this.state = { ...this.state, clientToken: nativeRes.client_token };
3409
+ const widget = await createKlarnaPaymentsWidget({
3410
+ clientToken: nativeRes.client_token,
3411
+ sessionId: nativeRes.session_id,
3412
+ categories: nativeRes.payment_method_categories ?? [],
3413
+ category: opts?.category,
3414
+ container
3415
+ });
3416
+ await widget.load();
3417
+ return {
3418
+ categories: widget.categories,
3419
+ shippings,
3420
+ get selected() {
3421
+ return widget.selected;
3422
+ },
3423
+ load: (category) => widget.load(category),
3424
+ authorize: async () => {
3425
+ try {
3426
+ const token = await widget.authorize();
3427
+ await this.completeKlarnaOrder(token, ctx, { authorizationToken: token });
3428
+ } catch (err) {
3429
+ if (typeof console !== "undefined") {
3430
+ console.warn("[VioCheckout] Klarna Payments authorize failed:", err);
3431
+ }
3432
+ this.dispatchEvent(
3433
+ new CustomEvent("payment-error", {
3434
+ detail: { method: "klarna", error: describeError(err) }
3435
+ })
3436
+ );
3437
+ }
3438
+ },
3439
+ unmount: () => {
3440
+ try {
3441
+ container.innerHTML = "";
3442
+ } catch {
3443
+ }
3444
+ }
3445
+ };
3446
+ }
3447
+ /** True if the Klarna Payments widget lib can load (UI gate). */
3448
+ async klarnaPaymentsAvailable() {
3449
+ return isKlarnaPaymentsAvailable();
3450
+ }
3451
+ /** Cart snapshot + chosen shipping line/amount, for express purchases. */
3452
+ buildKlarnaInstantContext(shippingId, availableShippings = []) {
3453
+ const base = this.buildKlarnaPendingContext();
3454
+ const options = availableShippings.length > 0 ? availableShippings : KLARNA_SHIPPING_OPTIONS;
3455
+ const shipping = (shippingId ? options.find((o) => o.id === shippingId) : void 0) ?? options.find((o) => o.preselected) ?? options[0];
3456
+ const orderLines = [...base.orderLines];
3457
+ if (shipping) {
3458
+ orderLines.push({
3459
+ name: `Frakt \u2013 ${shipping.method || shipping.name}`,
3460
+ quantity: 1,
3461
+ unit_price: shipping.price,
3462
+ total_amount: shipping.price,
3463
+ reference: `shipping:${shipping.id}`,
3464
+ type: "shipping_fee"
3465
+ });
3466
+ }
3467
+ return {
3468
+ ctx: {
3469
+ ...base,
3470
+ orderLines,
3471
+ orderAmount: base.orderAmount + (shipping?.price ?? 0)
3472
+ },
3473
+ shippingOptions: options
3474
+ };
3475
+ }
3476
+ /**
3477
+ * Express instant purchase — go straight to Klarna's own modal, no checkout
3478
+ * overlay. Creates a session (with shipping options), loads the widget into a
3479
+ * hidden container, then authorizes (which opens Klarna's confirm modal). On
3480
+ * approval creates the order → `payment-complete` (host shows confirmation);
3481
+ * on failure → `payment-error`. Call `open(sponsorId)` first.
3482
+ */
3483
+ async startKlarnaInstant() {
3484
+ if (!this.state) throw new Error("[VioCheckout] no active checkout \u2014 call open() first");
3485
+ const sponsorId = this.state.sponsorId;
3486
+ const fetchedShippings = await this.fetchAvailableShippings(sponsorId);
3487
+ const shippings = fetchedShippings.length > 0 ? fetchedShippings : KLARNA_SHIPPING_OPTIONS;
3488
+ const { ctx } = this.buildKlarnaInstantContext(void 0, shippings);
3489
+ let clientToken;
3490
+ let categories;
3491
+ try {
3492
+ let checkoutId = ctx.checkoutId ?? this.state.checkoutId;
3493
+ try {
3494
+ const checkoutRes = await this.createCheckout(sponsorId);
3495
+ if (checkoutRes?.id) {
3496
+ checkoutId = checkoutRes.id;
3497
+ ctx.checkoutId = checkoutRes.id;
3498
+ }
3499
+ } catch (err) {
3500
+ if (typeof console !== "undefined") {
3501
+ console.warn("[CheckoutManager] CreateCheckout before Klarna instant failed:", err);
3502
+ }
3503
+ }
3504
+ if (!checkoutId) {
3505
+ throw new Error("[CheckoutManager] no backend checkout for the Klarna session");
3506
+ }
3507
+ const address = this.state.address;
3508
+ const activeCurrency = getGlobalCurrency();
3509
+ this.state = { ...this.state, currency: activeCurrency };
3510
+ ctx.currency = activeCurrency;
3511
+ const variables = {
3512
+ checkoutId,
3513
+ countryCode: getGlobalCountryCode(),
3514
+ currency: activeCurrency,
3515
+ locale: getGlobalLocale(),
3516
+ returnUrl: resolveHttpsReturnUrl(null, {
3517
+ vio_payment: "success",
3518
+ vio_method: "klarna",
3519
+ vio_sponsor: sponsorId,
3520
+ checkout_id: checkoutId
3521
+ }),
3522
+ intent: "buy",
3523
+ customer: buildKlarnaCustomer(address),
3524
+ billingAddress: buildKlarnaAddress(address),
3525
+ shippingAddress: buildKlarnaAddress(address),
3526
+ autoCapture: true
3527
+ };
3528
+ const json = await executeKlarnaGraphQL(
3529
+ CREATE_PAYMENT_KLARNA_NATIVE_MUTATION,
3530
+ variables,
3531
+ sponsorId
3532
+ );
3533
+ const nativeRes = json?.data?.Payment?.CreatePaymentKlarnaNative;
3534
+ if (!nativeRes || !nativeRes.client_token) {
3535
+ throw new Error("Klarna instant session failed: missing client_token");
3536
+ }
3537
+ if (nativeRes.checkout_id) {
3538
+ ctx.checkoutId = nativeRes.checkout_id;
3539
+ this.state = { ...this.state, checkoutId: nativeRes.checkout_id };
3540
+ }
3541
+ if (nativeRes.session_id) {
3542
+ ctx.sessionId = nativeRes.session_id;
3543
+ this.state = { ...this.state, sessionId: nativeRes.session_id };
3544
+ }
3545
+ ctx.clientToken = nativeRes.client_token;
3546
+ this.state = { ...this.state, clientToken: nativeRes.client_token };
3547
+ clientToken = nativeRes.client_token;
3548
+ categories = nativeRes.payment_method_categories ?? [];
3549
+ } catch (err) {
3550
+ this.dispatchEvent(
3551
+ new CustomEvent("payment-error", {
3552
+ detail: { method: "klarna", error: describeError(err) }
3553
+ })
3554
+ );
3555
+ return;
3556
+ }
3557
+ const container = document.createElement("div");
3558
+ container.setAttribute("aria-hidden", "true");
3559
+ container.style.cssText = "position:fixed;left:-9999px;top:0;width:1px;height:1px;overflow:hidden;";
3560
+ document.body.appendChild(container);
3561
+ try {
3562
+ const widget = await createKlarnaPaymentsWidget({
3563
+ clientToken,
3564
+ sessionId: ctx.sessionId,
3565
+ categories,
3566
+ container
3567
+ });
3568
+ await widget.load();
3569
+ const token = await widget.authorize();
3570
+ await this.completeKlarnaOrder(token, ctx, { authorizationToken: token });
3571
+ } catch (err) {
3572
+ if (typeof console !== "undefined") {
3573
+ console.warn("[VioCheckout] Klarna instant failed:", err);
3574
+ }
3575
+ this.dispatchEvent(
3576
+ new CustomEvent("payment-error", {
3577
+ detail: { method: "klarna", error: describeError(err) }
3578
+ })
3579
+ );
3580
+ } finally {
3581
+ try {
3582
+ container.remove();
3583
+ } catch {
3584
+ }
3585
+ }
3586
+ }
3587
+ /**
3588
+ * REDIRECT path: call once on app load. If a Klarna purchase was launched
3589
+ * (pending context persisted) and we're back with a token — from the URL or
3590
+ * the SDK's resumed `complete` event — finish creating the order + confirm.
3591
+ */
3592
+ async resumeKlarnaReturn() {
3593
+ if (typeof sessionStorage === "undefined") return;
3594
+ const raw = sessionStorage.getItem(KLARNA_PENDING_KEY);
3595
+ if (!raw) return;
3596
+ let pending;
3597
+ try {
3598
+ pending = JSON.parse(raw);
3599
+ } catch {
3600
+ sessionStorage.removeItem(KLARNA_PENDING_KEY);
3601
+ return;
3602
+ }
3603
+ const fromUrl = readKlarnaTokenFromUrl();
3604
+ if (fromUrl) {
3605
+ cleanUrlParams([fromUrl.param]);
3606
+ await this.completeKlarnaOrder(fromUrl.token, pending, { authorizationToken: fromUrl.token });
3607
+ return;
3608
+ }
3609
+ if (typeof window !== "undefined" && typeof console !== "undefined" && window.location.search) {
3610
+ console.warn(
3611
+ "[VioCheckout] Klarna return: no known token param. URL was:",
3612
+ window.location.search
3613
+ );
3614
+ }
3615
+ const cfg = Configuration.isInitialized ? Configuration.get() : null;
3616
+ if (!cfg?.klarnaClientId) return;
3617
+ await listenForKlarnaCompletion({
3618
+ config: { clientId: cfg.klarnaClientId, locale: getGlobalLocale(), environment: cfg.klarnaEnvironment },
3619
+ onComplete: (result) => {
3620
+ void this.completeKlarnaOrder(result.authorizationToken, pending, result);
3621
+ },
3622
+ onError: (err) => {
3623
+ if (typeof console !== "undefined") {
3624
+ console.warn("[VioCheckout] Klarna resume error:", err);
3625
+ }
3626
+ }
3627
+ });
3628
+ }
3629
+ /** Snapshot of the active cart for redirect recovery (minor units). */
3630
+ buildKlarnaPendingContext() {
3631
+ if (!this.state) throw new Error("[VioCheckout] no active checkout");
3632
+ const currency = this.state.currency;
3633
+ return {
3634
+ sponsorId: this.state.sponsorId,
3635
+ currency,
3636
+ orderAmount: toSmallestUnit(this.state.subtotal, currency),
3637
+ orderLines: this.items.map((item) => {
3638
+ const unitPrice = toSmallestUnit(item.unitPrice, currency);
3639
+ return {
3640
+ name: item.name || item.brand || "Produkt",
3641
+ quantity: item.quantity,
3642
+ unit_price: unitPrice,
3643
+ total_amount: unitPrice * item.quantity,
3644
+ reference: String(item.productId)
3645
+ };
3646
+ })
3647
+ };
3648
+ }
3649
+ /**
3650
+ * POST the token + order lines to vio-backend (it holds the secret API key
3651
+ * and creates the real Klarna order). On success → `payment-complete`; on
3652
+ * failure → `payment-error`. Idempotent: clears the pending key so the
3653
+ * popup and redirect paths can't double-fire.
3654
+ */
3655
+ async completeKlarnaOrder(authorizationToken, ctx, result) {
3656
+ if (this.klarnaOrderInFlight) return;
3657
+ this.klarnaOrderInFlight = true;
3658
+ if (typeof sessionStorage !== "undefined" && sessionStorage.getItem("vio.klarna.confirmed.v1") === authorizationToken) {
3659
+ this.klarnaOrderInFlight = false;
3660
+ return;
3661
+ }
3662
+ try {
3663
+ const checkoutId = ctx?.checkoutId ?? this.state?.checkoutId ?? String(ctx?.sponsorId ?? this.state?.sponsorId ?? "");
3664
+ const address = this.state?.address;
3665
+ const variables = {
3666
+ checkoutId,
3667
+ authorizationToken,
3668
+ autoCapture: true,
3669
+ billingAddress: buildKlarnaAddress(address),
3670
+ shippingAddress: buildKlarnaAddress(address),
3671
+ customer: buildKlarnaCustomer(address)
3672
+ };
3673
+ const json = await executeKlarnaGraphQL(
3674
+ CONFIRM_PAYMENT_KLARNA_NATIVE_MUTATION,
3675
+ variables,
3676
+ ctx?.sponsorId ?? this.state?.sponsorId
3677
+ );
3678
+ const confirmRes = json?.data?.Payment?.ConfirmPaymentKlarnaNative;
3679
+ if (!confirmRes) {
3680
+ throw new Error("Klarna order confirmation failed: missing response data");
3681
+ }
3682
+ const orderData = confirmRes.order || confirmRes;
3683
+ if (typeof sessionStorage !== "undefined") {
3684
+ sessionStorage.setItem("vio.klarna.confirmed.v1", authorizationToken);
3685
+ sessionStorage.removeItem(KLARNA_PENDING_KEY);
3686
+ }
3687
+ const state = this.state ?? { sponsorId: ctx.sponsorId, subtotal: ctx.orderAmount, currency: ctx.currency };
3688
+ this.dispatchEvent(
3689
+ new CustomEvent("payment-complete", {
3690
+ detail: {
3691
+ method: "klarna",
3692
+ state,
3693
+ // chargedTotal in MAJOR units (incl. shipping for express) for the
3694
+ // confirmation screen. ctx.orderAmount is minor (øre) → /100.
3695
+ // Prefer the amount Klarna actually charged (includes shipping in
3696
+ // every flow); the local ctx misses shipping outside express.
3697
+ result: {
3698
+ ...result,
3699
+ order: orderData,
3700
+ chargedTotal: typeof orderData?.order_amount === "number" ? orderData.order_amount / 100 : ctx.orderAmount / 100
3701
+ }
3702
+ }
3703
+ })
3704
+ );
3705
+ } catch (err) {
3706
+ if (typeof console !== "undefined") {
3707
+ console.error("[VioCheckout] Klarna order creation failed:", err);
3708
+ }
3709
+ this.dispatchEvent(
3710
+ new CustomEvent("payment-error", {
3711
+ detail: { method: "klarna", error: err instanceof Error ? err.message : String(err) }
3712
+ })
3713
+ );
3714
+ } finally {
3715
+ this.klarnaOrderInFlight = false;
3716
+ }
3717
+ }
3718
+ // MARK: - Accessors
3719
+ get currentState() {
3720
+ return this.state;
3721
+ }
3722
+ get items() {
3723
+ if (!this.state) return [];
3724
+ return this.cartManager.getCart(this.state.sponsorId)?.items ?? [];
3725
+ }
3726
+ // MARK: - Internals
3727
+ /**
3728
+ * Resolve the Stripe Connect account for the active sponsor — for now
3729
+ * pulled from bootstrap cache if present. Returns undefined if not set
3730
+ * (Stripe will route to the platform account in that case).
3731
+ */
3732
+ findStripeConnectAccount() {
3733
+ return void 0;
3734
+ }
3735
+ emit() {
3736
+ this.dispatchEvent(
3737
+ new CustomEvent("change", { detail: { state: this.state } })
3738
+ );
3739
+ }
3740
+ };
3741
+ function toSmallestUnit(amount, currency) {
3742
+ const zeroDecimal = /* @__PURE__ */ new Set(["JPY", "KRW", "VND"]);
3743
+ if (zeroDecimal.has(currency.toUpperCase())) return Math.round(amount);
3744
+ return Math.round(amount * 100);
3745
+ }
3746
+
3747
+ // src/core/client.ts
3748
+ var VioFacade = class {
3749
+ constructor() {
3750
+ this.apiInstance = null;
3751
+ this.commerceClients = /* @__PURE__ */ new Map();
3752
+ this.bootstrapPromise = null;
3753
+ this.cachedBootstrap = null;
3754
+ /** Last bootstrap failure — short backoff so cart mutations don't hammer a
3755
+ * dead endpoint on every call. */
3756
+ this.bootstrapFailedAt = 0;
3757
+ // Client-side managers — created lazily on first access. They don't
3758
+ // need Vio.init() to work (state is purely local).
3759
+ this.cartManager = null;
3760
+ this.checkoutManager = null;
3761
+ }
3762
+ init(config) {
3763
+ Configuration.init(config);
3764
+ this.apiInstance = null;
3765
+ this.commerceClients.clear();
3766
+ this.bootstrapPromise = null;
3767
+ this.cachedBootstrap = null;
3768
+ this.bootstrapFailedAt = 0;
3769
+ this.checkoutManager?.clearPaymentMethodsCache();
3770
+ }
3771
+ get isInitialized() {
3772
+ return Configuration.isInitialized;
3773
+ }
3774
+ get config() {
3775
+ return Configuration.get();
3776
+ }
3777
+ // MARK: - Client-side managers (no init required)
3778
+ /** Multi-sponsor cart manager. Reactive — listen via addEventListener('change', ...). */
3779
+ get cart() {
3780
+ if (!this.cartManager) this.cartManager = new CartManager();
3781
+ return this.cartManager;
3782
+ }
3783
+ /** Checkout flow manager. Subscribes to cart state via the CartManager. */
3784
+ get checkout() {
3785
+ if (!this.checkoutManager) this.checkoutManager = new CheckoutManager(this.cart);
3786
+ return this.checkoutManager;
3787
+ }
3788
+ // MARK: - Backend-bound
3789
+ /** Lazy VioApi for REST endpoints on `api-dev.vio.live`. */
3790
+ get api() {
3791
+ if (!this.apiInstance) {
3792
+ const cfg = Configuration.get();
3793
+ this.apiInstance = createVioApi({ apiBase: cfg.apiBase, apiKey: cfg.apiKey });
3794
+ }
3795
+ return this.apiInstance;
3796
+ }
3797
+ /**
3798
+ * Bootstrap — fetches client config and caches it. Dedupes in-flight
3799
+ * concurrent requests.
3800
+ */
3801
+ async bootstrap() {
3802
+ if (this.cachedBootstrap) return this.cachedBootstrap;
3803
+ if (this.bootstrapPromise) return this.bootstrapPromise;
3804
+ if (this.bootstrapFailedAt && Date.now() - this.bootstrapFailedAt < 5e3) {
3805
+ throw new Error("[Vio] bootstrap recently failed \u2014 backing off");
3806
+ }
3807
+ this.bootstrapPromise = this.api.bootstrap().then((res) => {
3808
+ this.cachedBootstrap = res;
3809
+ this.bootstrapFailedAt = 0;
3810
+ return res;
3811
+ }).catch((err) => {
3812
+ this.bootstrapFailedAt = Date.now();
3813
+ throw err;
3814
+ }).finally(() => {
3815
+ this.bootstrapPromise = null;
3816
+ });
3817
+ return this.bootstrapPromise;
3818
+ }
3819
+ get bootstrapCache() {
3820
+ return this.cachedBootstrap;
3821
+ }
3822
+ /**
3823
+ * Get a CommerceClient scoped to a specific sponsor (via bootstrap).
3824
+ * Throws if bootstrap hasn't been called or the sponsor is unknown.
3825
+ */
3826
+ commerceFor(sponsorId) {
3827
+ sponsorId = Number(sponsorId);
3828
+ const existing = this.commerceClients.get(sponsorId);
3829
+ if (existing) return existing;
3830
+ const sponsor = this.findSponsor(sponsorId);
3831
+ if (!sponsor) {
3832
+ throw new Error(
3833
+ `[Vio] Sponsor ${sponsorId} not found in bootstrap. Call Vio.bootstrap() first or pass commerce-key directly.`
3834
+ );
3835
+ }
3836
+ const cfg = Configuration.get();
3837
+ const client = createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: sponsor.commerce.apiKey });
3838
+ this.commerceClients.set(sponsorId, client);
3839
+ return client;
3840
+ }
3841
+ /** Get a CommerceClient using an explicit commerce apiKey (skip bootstrap). */
3842
+ commerceWithKey(commerceApiKey) {
3843
+ const cfg = Configuration.get();
3844
+ return createCommerceClient({ endpoint: cfg.graphQLBase, apiKey: commerceApiKey });
3845
+ }
3846
+ findSponsor(sponsorId) {
3847
+ const boot = this.cachedBootstrap;
3848
+ if (!boot) return void 0;
3849
+ const id = Number(sponsorId);
3850
+ if (Number(boot.primarySponsor?.id) === id) return boot.primarySponsor;
3851
+ return boot.secondarySponsors?.find((s) => Number(s.id) === id);
3852
+ }
3853
+ };
3854
+ var _vioGlobal = globalThis;
3855
+ var Vio = _vioGlobal.__VIO_FACADE__ ?? (_vioGlobal.__VIO_FACADE__ = new VioFacade());
3856
+
3857
+ export { ADD_ITEM_MUTATION, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, KLARNA_SHIPPING_OPTIONS, UPDATE_CHECKOUT_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, Vio, VioApi, VioApiError, __decorateClass, addItem, checkApplePayAvailability, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, executeCartGraphQL, formatPrice, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, primaryImageUrl, showApplePaySheet, updateCheckout, updateItem, updateShippingsBySupplier };
3858
+ //# sourceMappingURL=chunk-NP2KCM7X.js.map
3859
+ //# sourceMappingURL=chunk-NP2KCM7X.js.map