@vio-live/web-sdk 0.11.1 → 0.21.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.
@@ -13,10 +13,12 @@ var __decorateClass = (decorators, target, key, kind) => {
13
13
 
14
14
  // src/core/configuration.ts
15
15
  var DEFAULT_URLS = {
16
+ // The Vio Backend development environment was removed on 2026-09-16; its REST
17
+ // and events hosts now point at staging (Commerce GraphQL dev is unaffected).
16
18
  development: {
17
- apiBase: "https://api-dev.vio.live",
19
+ apiBase: "https://api-staging.vio.live",
18
20
  graphQLBase: "https://graph-ql-dev.vio.live",
19
- eventsBase: "https://events-dev.vio.live"
21
+ eventsBase: "https://events-staging.vio.live"
20
22
  },
21
23
  // No separate staging GraphQL deployment yet — shares dev's.
22
24
  testing: {
@@ -351,6 +353,19 @@ var GET_PRODUCTS_BY_IDS = (
351
353
  }
352
354
  `
353
355
  );
356
+ var GET_PRODUCT_SELLER = (
357
+ /* GraphQL */
358
+ `
359
+ query GetProductSeller($productIds: [Int!], $useCache: Boolean!) {
360
+ Channel {
361
+ GetProductsByIds(product_ids: $productIds, useCache: $useCache) {
362
+ id
363
+ supplier_company
364
+ }
365
+ }
366
+ }
367
+ `
368
+ );
354
369
  var ChannelProduct = class {
355
370
  constructor(client) {
356
371
  this.client = client;
@@ -372,6 +387,24 @@ var ChannelProduct = class {
372
387
  const data = await this.client.request(GET_PRODUCTS_BY_IDS, variables);
373
388
  return data.Channel?.GetProductsByIds ?? [];
374
389
  }
390
+ /**
391
+ * The seller's Company name for one product, or null (none set, or an API
392
+ * that does not have the field yet). Never throws.
393
+ *
394
+ * Never from the cache, on purpose: graphql caches product answers by
395
+ * path whatever fields the query asked, so this narrow query, written to
396
+ * the cache, would hand the next full product query a product with no
397
+ * price and no images. `useCache: false` neither reads nor writes it.
398
+ */
399
+ async getSellerCompany(productId) {
400
+ try {
401
+ const data = await this.client.request(GET_PRODUCT_SELLER, { productIds: [productId], useCache: false });
402
+ const name = String(data.Channel?.GetProductsByIds?.[0]?.supplier_company ?? "").trim();
403
+ return name || null;
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
375
408
  };
376
409
  var Channel = class {
377
410
  constructor(client) {
@@ -1175,6 +1208,7 @@ query GetCheckout($checkoutId: String!) {
1175
1208
  status
1176
1209
  checkout_url
1177
1210
  origin_payment_id
1211
+ duplicate_of
1178
1212
  billing_address {
1179
1213
  id
1180
1214
  first_name
@@ -1306,18 +1340,18 @@ query Config {
1306
1340
  `;
1307
1341
  async function getCartGraphQLOptions(sponsorId) {
1308
1342
  const facade = globalThis.__VIO_FACADE__;
1309
- if (facade && typeof facade.bootstrap === "function" && !facade.bootstrapCache) {
1343
+ if (!Configuration.isInitialized) {
1344
+ await Configuration.whenReady();
1345
+ }
1346
+ if (Configuration.isInitialized && facade && typeof facade.bootstrap === "function" && !facade.bootstrapCache) {
1310
1347
  try {
1311
1348
  await facade.bootstrap();
1312
1349
  } catch (err) {
1313
1350
  if (typeof console !== "undefined") {
1314
- console.warn("[Vio] getCartGraphQLOptions bootstrap await failed:", err);
1351
+ console.warn("[Vio] getCartGraphQLOptions bootstrap failed:", err);
1315
1352
  }
1316
1353
  }
1317
1354
  }
1318
- if (!Configuration.isInitialized) {
1319
- await Configuration.whenReady();
1320
- }
1321
1355
  const cfg = Configuration.isInitialized ? Configuration.get() : { apiKey: void 0, graphQLBase: "https://graph-ql-dev.vio.live" };
1322
1356
  const anyCfg = cfg;
1323
1357
  let commerceKey = anyCfg.apiKey;
@@ -1329,6 +1363,11 @@ async function getCartGraphQLOptions(sponsorId) {
1329
1363
  sponsorResolved = true;
1330
1364
  }
1331
1365
  }
1366
+ if (sponsorId && !sponsorResolved && typeof console !== "undefined") {
1367
+ console.warn(
1368
+ `[Vio] no commerce key for sponsor ${sponsorId} \u2014 falling back to the platform apiKey, which the commerce API will reject. Is the sponsor configured, and did Vio.init run before this call?`
1369
+ );
1370
+ }
1332
1371
  return {
1333
1372
  endpoint: anyCfg.graphQLBase || "https://graph-ql-dev.vio.live",
1334
1373
  apiKey: anyCfg.apiKey,
@@ -1420,6 +1459,21 @@ async function getCheckout(checkoutId, options) {
1420
1459
  const json = await executeCartGraphQL(GET_CHECKOUT_QUERY, { checkoutId }, options);
1421
1460
  return json?.data?.Checkout?.GetCheckout;
1422
1461
  }
1462
+ var GET_CHECKOUT_ORDER_NUMBER_QUERY = `
1463
+ query GetCheckoutOrderNumber($checkoutId: String!) {
1464
+ Checkout {
1465
+ GetCheckout(checkout_id: $checkoutId) {
1466
+ id
1467
+ order_id
1468
+ }
1469
+ }
1470
+ }
1471
+ `;
1472
+ async function getCheckoutOrderNumber(checkoutId, options) {
1473
+ const json = await executeCartGraphQL(GET_CHECKOUT_ORDER_NUMBER_QUERY, { checkoutId }, options);
1474
+ const n = Number(json?.data?.Checkout?.GetCheckout?.order_id);
1475
+ return Number.isInteger(n) && n > 0 ? n : null;
1476
+ }
1423
1477
  async function getAvailablePaymentMethods(options) {
1424
1478
  const json = await executeCartGraphQL(GET_AVAILABLE_PAYMENT_METHODS_QUERY, {}, options);
1425
1479
  return json?.data?.Payment?.GetAvailablePaymentMethods;
@@ -1469,10 +1523,13 @@ async function confirmPaymentApplePay(variables, options) {
1469
1523
 
1470
1524
  // src/core/checkout/payments/vipps.ts
1471
1525
  var CREATE_PAYMENT_VIPPS_MUTATION = `
1472
- mutation CreatePaymentVipps($checkoutId: String!, $email: String!, $returnUrl: String!) {
1526
+ mutation CreatePaymentVipps($checkoutId: String!, $email: String, $returnUrl: String!, $express: Boolean) {
1473
1527
  Payment {
1474
- CreatePaymentVipps(checkout_id: $checkoutId, email: $email, return_url: $returnUrl) {
1528
+ CreatePaymentVipps(checkout_id: $checkoutId, email: $email, return_url: $returnUrl, express: $express) {
1475
1529
  payment_url
1530
+ reference
1531
+ express
1532
+ reason
1476
1533
  }
1477
1534
  }
1478
1535
  }
@@ -1482,6 +1539,7 @@ query GetVippsStatus($checkoutId: String!) {
1482
1539
  Payment {
1483
1540
  GetVippsStatus(checkout_id: $checkoutId) {
1484
1541
  state
1542
+ duplicate_of
1485
1543
  }
1486
1544
  }
1487
1545
  }
@@ -2159,6 +2217,97 @@ async function prepareApplePay(config) {
2159
2217
  }
2160
2218
  }
2161
2219
 
2220
+ // src/core/version.ts
2221
+ var SDK_VERSION = "0.21.0";
2222
+
2223
+ // src/core/checkout/payments/stripe-embedded.ts
2224
+ var CREATE_PAYMENT_INTENT_STRIPE_MUTATION = `
2225
+ mutation CreatePaymentIntentStripe($checkoutId: String!) {
2226
+ Payment {
2227
+ CreatePaymentIntentStripe(checkout_id: $checkoutId) {
2228
+ client_secret
2229
+ publishable_key
2230
+ customer
2231
+ }
2232
+ }
2233
+ }
2234
+ `;
2235
+ async function createStripeIntent(checkoutId, options) {
2236
+ const json = await executeCartGraphQL(
2237
+ CREATE_PAYMENT_INTENT_STRIPE_MUTATION,
2238
+ { checkoutId },
2239
+ options
2240
+ );
2241
+ return json?.data?.Payment?.CreatePaymentIntentStripe ?? null;
2242
+ }
2243
+ var SETTLED = /* @__PURE__ */ new Set(["succeeded"]);
2244
+ var IN_FLIGHT = /* @__PURE__ */ new Set(["processing", "requires_capture"]);
2245
+ async function mountStripeElement(container, intent, options) {
2246
+ if (!intent?.client_secret || !intent.publishable_key) {
2247
+ throw new Error("[Stripe] the payment is missing its client secret or publishable key");
2248
+ }
2249
+ await loadStripeJs();
2250
+ const Stripe = globalThis.Stripe;
2251
+ if (typeof Stripe !== "function") {
2252
+ throw new Error("[Stripe] stripe.js loaded but window.Stripe is missing");
2253
+ }
2254
+ const stripe = intent.stripe_account ? Stripe(intent.publishable_key, { stripeAccount: intent.stripe_account }) : Stripe(intent.publishable_key);
2255
+ const appearance = { theme: "stripe" };
2256
+ const variables = {};
2257
+ if (options.theme?.accent) variables.colorPrimary = options.theme.accent;
2258
+ if (options.theme?.radiusMd) variables.borderRadius = options.theme.radiusMd;
2259
+ if (Object.keys(variables).length > 0) appearance.variables = variables;
2260
+ const elements = stripe.elements({
2261
+ clientSecret: intent.client_secret,
2262
+ appearance,
2263
+ locale: options.locale ?? "nb"
2264
+ });
2265
+ const element = elements.create("payment", { layout: "tabs" });
2266
+ if (options.onReady) element.on("ready", () => options.onReady?.());
2267
+ if (options.onLoadError) {
2268
+ element.on(
2269
+ "loaderror",
2270
+ (event) => options.onLoadError?.(String(event?.error?.message ?? "Stripe could not load the form"))
2271
+ );
2272
+ }
2273
+ container.innerHTML = "";
2274
+ element.mount(container);
2275
+ let confirming = false;
2276
+ return {
2277
+ async confirm() {
2278
+ if (confirming) return { status: "pending" };
2279
+ confirming = true;
2280
+ try {
2281
+ const result = await stripe.confirmPayment({
2282
+ elements,
2283
+ confirmParams: { return_url: options.returnUrl },
2284
+ redirect: "if_required"
2285
+ });
2286
+ if (result?.error) {
2287
+ return {
2288
+ status: "failed",
2289
+ message: String(result.error.message ?? ""),
2290
+ code: String(result.error.code ?? "")
2291
+ };
2292
+ }
2293
+ const paymentIntent = result?.paymentIntent;
2294
+ const status = String(paymentIntent?.status ?? "");
2295
+ if (SETTLED.has(status)) return { status: "succeeded", paymentIntentId: paymentIntent?.id };
2296
+ if (IN_FLIGHT.has(status)) return { status: "pending", paymentIntentId: paymentIntent?.id };
2297
+ return { status: "failed", code: status };
2298
+ } finally {
2299
+ confirming = false;
2300
+ }
2301
+ },
2302
+ unmount() {
2303
+ try {
2304
+ element.unmount();
2305
+ } catch {
2306
+ }
2307
+ }
2308
+ };
2309
+ }
2310
+
2162
2311
  // src/core/checkout/payments/klarna.ts
2163
2312
  var KLARNA_SDK_URL = "https://js.klarna.com/web-sdk/v2/klarna.mjs";
2164
2313
  var sdkCache = /* @__PURE__ */ new Map();
@@ -2519,15 +2668,23 @@ async function createKlarnaPaymentsWidget(input) {
2519
2668
  }
2520
2669
 
2521
2670
  // src/core/checkout/payments/kustom.ts
2522
- var CREATE_PAYMENT_KUSTOM_MUTATION = `
2523
- mutation CreatePaymentKustom($checkoutId: String!, $countryCode: String!, $href: String!, $email: String) {
2524
- Payment {
2525
- CreatePaymentKustom(checkout_id: $checkoutId, country_code: $countryCode, href: $href, email: $email) {
2671
+ var KUSTOM_ORDER_FIELDS = `
2526
2672
  order_id
2527
2673
  status
2528
2674
  html_snippet
2529
2675
  purchase_country
2530
2676
  purchase_currency
2677
+ checkout_id
2678
+ environment
2679
+ total_price
2680
+ shipping_name
2681
+ shipping_price
2682
+ order_created
2683
+ reference`;
2684
+ var CREATE_PAYMENT_KUSTOM_MUTATION = `
2685
+ mutation CreatePaymentKustom($checkoutId: String!, $countryCode: String!, $href: String!, $email: String, $client: String) {
2686
+ Payment {
2687
+ CreatePaymentKustom(checkout_id: $checkoutId, country_code: $countryCode, href: $href, email: $email, client: $client) {${KUSTOM_ORDER_FIELDS}
2531
2688
  }
2532
2689
  }
2533
2690
  }
@@ -2535,12 +2692,15 @@ mutation CreatePaymentKustom($checkoutId: String!, $countryCode: String!, $href:
2535
2692
  var GET_KUSTOM_ORDER_QUERY = `
2536
2693
  query GetKustomOrder($orderId: String!) {
2537
2694
  Payment {
2538
- GetKustomOrder(order_id: $orderId) {
2539
- order_id
2540
- status
2541
- html_snippet
2542
- purchase_country
2543
- purchase_currency
2695
+ GetKustomOrder(order_id: $orderId) {${KUSTOM_ORDER_FIELDS}
2696
+ }
2697
+ }
2698
+ }
2699
+ `;
2700
+ var SYNC_PAYMENT_KUSTOM_MUTATION = `
2701
+ mutation SyncPaymentKustom($checkoutId: String!) {
2702
+ Payment {
2703
+ SyncPaymentKustom(checkout_id: $checkoutId) {${KUSTOM_ORDER_FIELDS}
2544
2704
  }
2545
2705
  }
2546
2706
  }
@@ -2553,6 +2713,10 @@ async function getKustomOrder(orderId, options) {
2553
2713
  const json = await executeCartGraphQL(GET_KUSTOM_ORDER_QUERY, { orderId }, options);
2554
2714
  return json?.data?.Payment?.GetKustomOrder ?? null;
2555
2715
  }
2716
+ async function syncPaymentKustom(checkoutId, options) {
2717
+ const json = await executeCartGraphQL(SYNC_PAYMENT_KUSTOM_MUTATION, { checkoutId }, options);
2718
+ return json?.data?.Payment?.SyncPaymentKustom ?? null;
2719
+ }
2556
2720
  function kustomCleanHref() {
2557
2721
  if (typeof window === "undefined" || !window.location) return "";
2558
2722
  return `${window.location.origin}${window.location.pathname}`;
@@ -2569,6 +2733,64 @@ function renderKustomSnippet(container, htmlSnippet) {
2569
2733
  oldScript.parentNode?.replaceChild(newScript, oldScript);
2570
2734
  }
2571
2735
  }
2736
+ function kustomApi() {
2737
+ if (typeof window === "undefined") return null;
2738
+ const fn = window._klarnaCheckout;
2739
+ return typeof fn === "function" ? fn : null;
2740
+ }
2741
+ function suspendKustom() {
2742
+ const api = kustomApi();
2743
+ if (!api) return false;
2744
+ api((a) => a.suspend());
2745
+ return true;
2746
+ }
2747
+ function resumeKustom() {
2748
+ const api = kustomApi();
2749
+ if (!api) return false;
2750
+ api((a) => a.resume());
2751
+ return true;
2752
+ }
2753
+ function installKustomListeners(handlers) {
2754
+ let alive = true;
2755
+ const api = kustomApi();
2756
+ if (api) {
2757
+ api((a) => {
2758
+ a.on({
2759
+ load: (data) => alive && handlers.onLoad?.(data),
2760
+ order_total_change: (data) => {
2761
+ const total = Number(data?.order_total);
2762
+ if (alive && Number.isFinite(total)) handlers.onOrderTotalChange?.(total);
2763
+ },
2764
+ shipping_option_change: (data) => alive && handlers.onShippingOptionChange?.(data ?? {}),
2765
+ can_not_complete_order: () => alive && handlers.onCannotComplete?.(),
2766
+ network_error: () => alive && handlers.onNetworkError?.(),
2767
+ redirect_initiated: () => alive && handlers.onRedirectInitiated?.()
2768
+ });
2769
+ });
2770
+ }
2771
+ return {
2772
+ destroy() {
2773
+ alive = false;
2774
+ }
2775
+ };
2776
+ }
2777
+ var KUSTOM_RETURN_QUERY_KEYS = [
2778
+ "order_id",
2779
+ "payment_processor",
2780
+ "checkout_id",
2781
+ "vio_payment",
2782
+ "vio_method"
2783
+ ];
2784
+ function kustomReturnOutcome(params) {
2785
+ const orderId = params.get("order_id") || "";
2786
+ if (params.get("payment_processor") === "KUSTOM" && orderId) {
2787
+ return { kind: "confirmation", orderId, checkoutId: params.get("checkout_id") || void 0 };
2788
+ }
2789
+ if (params.get("vio_payment") === "rejected" && params.get("vio_method") === "kustom") {
2790
+ return { kind: "rejected", checkoutId: params.get("checkout_id") || void 0 };
2791
+ }
2792
+ return null;
2793
+ }
2572
2794
 
2573
2795
  // src/core/checkout/payments/qliro.ts
2574
2796
  function readVioTheme() {
@@ -2654,7 +2876,8 @@ function installQliroListeners(options) {
2654
2876
  const win = globalThis;
2655
2877
  let q1 = null;
2656
2878
  let destroyed = false;
2657
- let pending = null;
2879
+ let orderUpdatedRegistered = false;
2880
+ let pending2 = null;
2658
2881
  const emit = (event) => {
2659
2882
  try {
2660
2883
  onEvent?.(event);
@@ -2662,10 +2885,10 @@ function installQliroListeners(options) {
2662
2885
  }
2663
2886
  };
2664
2887
  const settle = () => {
2665
- if (!pending) return;
2666
- clearTimeout(pending.timer);
2667
- const { resolve } = pending;
2668
- pending = null;
2888
+ if (!pending2) return;
2889
+ clearTimeout(pending2.timer);
2890
+ const { resolve } = pending2;
2891
+ pending2 = null;
2669
2892
  try {
2670
2893
  q1?.unlock();
2671
2894
  } catch {
@@ -2677,8 +2900,8 @@ function installQliroListeners(options) {
2677
2900
  return !echo || echo === version;
2678
2901
  };
2679
2902
  const settleIfAcknowledged = () => {
2680
- if (!pending) return;
2681
- if (pending.echoes.some((echo) => acknowledges(echo, pending.version))) {
2903
+ if (!pending2) return;
2904
+ if (pending2.echoes.some((echo) => acknowledges(echo, pending2.version))) {
2682
2905
  settle();
2683
2906
  }
2684
2907
  };
@@ -2691,12 +2914,6 @@ function installQliroListeners(options) {
2691
2914
  if (destroyed) return;
2692
2915
  q1 = instance;
2693
2916
  instance.onCheckoutLoaded(() => emit({ type: "loaded" }));
2694
- instance.onOrderUpdated((order) => {
2695
- if (!pending) return;
2696
- const echoed = order?.merchantUpdateVersion ?? order?.MerchantUpdateVersion ?? null;
2697
- pending.echoes.push(echoed);
2698
- settleIfAcknowledged();
2699
- });
2700
2917
  instance.onShippingMethodChanged(
2701
2918
  (shipping) => emit({ type: "shipping-changed", shipping })
2702
2919
  );
@@ -2720,13 +2937,22 @@ function installQliroListeners(options) {
2720
2937
  const controller = {
2721
2938
  async sync() {
2722
2939
  if (destroyed) return null;
2723
- if (pending) settle();
2940
+ if (pending2) settle();
2724
2941
  try {
2725
2942
  q1?.lock();
2943
+ if (q1 && !orderUpdatedRegistered) {
2944
+ orderUpdatedRegistered = true;
2945
+ q1.onOrderUpdated((order) => {
2946
+ if (!pending2) return;
2947
+ const echoed = order?.merchantUpdateVersion ?? order?.MerchantUpdateVersion ?? null;
2948
+ pending2.echoes.push(echoed);
2949
+ settleIfAcknowledged();
2950
+ });
2951
+ }
2726
2952
  } catch {
2727
2953
  }
2728
2954
  const settled = new Promise((resolve) => {
2729
- pending = {
2955
+ pending2 = {
2730
2956
  version: null,
2731
2957
  echoes: [],
2732
2958
  resolve,
@@ -2747,8 +2973,8 @@ function installQliroListeners(options) {
2747
2973
  await settled;
2748
2974
  return result;
2749
2975
  }
2750
- if (pending) {
2751
- pending.version = result.update_version;
2976
+ if (pending2) {
2977
+ pending2.version = result.update_version;
2752
2978
  settleIfAcknowledged();
2753
2979
  }
2754
2980
  await settled;
@@ -2805,6 +3031,441 @@ async function getWalleyOrder(checkoutId, options) {
2805
3031
  }
2806
3032
  var WALLEY_PURCHASE_COMPLETED_EVENT = "walleyCheckoutPurchaseCompleted";
2807
3033
 
3034
+ // src/core/checkout/payments/nexi.ts
3035
+ var NEXI_SHIPPING_FIELDS = `
3036
+ ok
3037
+ reason
3038
+ order_id
3039
+ total_price
3040
+ shipping_name
3041
+ shipping_price
3042
+ shipping_id
3043
+ options { id name price }
3044
+ country
3045
+ postal_code
3046
+ address_source
3047
+ changed`;
3048
+ var NEXI_ORDER_FIELDS = `
3049
+ order_id
3050
+ status
3051
+ checkout_key
3052
+ checkout_js_url
3053
+ purchase_country
3054
+ purchase_currency
3055
+ total_price
3056
+ payment_method
3057
+ card_last4
3058
+ email
3059
+ shipping {${NEXI_SHIPPING_FIELDS}
3060
+ }`;
3061
+ var CREATE_PAYMENT_NEXI_MUTATION = `
3062
+ mutation CreatePaymentNexi($checkoutId: String!, $countryCode: String!, $href: String!, $email: String) {
3063
+ Payment {
3064
+ CreatePaymentNexi(checkout_id: $checkoutId, country_code: $countryCode, href: $href, email: $email) {${NEXI_ORDER_FIELDS}
3065
+ }
3066
+ }
3067
+ }
3068
+ `;
3069
+ var GET_NEXI_ORDER_QUERY = `
3070
+ query GetNexiOrder($checkoutId: String!) {
3071
+ Payment {
3072
+ GetNexiOrder(checkout_id: $checkoutId) {${NEXI_ORDER_FIELDS}
3073
+ }
3074
+ }
3075
+ }
3076
+ `;
3077
+ var UPDATE_NEXI_SHIPPING_MUTATION = `
3078
+ mutation UpdateNexiShipping($checkoutId: String!, $countryCode: String!, $postalCode: String, $shippingId: String) {
3079
+ Payment {
3080
+ UpdateNexiShipping(checkout_id: $checkoutId, country_code: $countryCode, postal_code: $postalCode, shipping_id: $shippingId) {${NEXI_SHIPPING_FIELDS}
3081
+ }
3082
+ }
3083
+ }
3084
+ `;
3085
+ async function createPaymentNexi(variables, options) {
3086
+ const json = await executeCartGraphQL(CREATE_PAYMENT_NEXI_MUTATION, variables, options);
3087
+ return json?.data?.Payment?.CreatePaymentNexi ?? null;
3088
+ }
3089
+ async function getNexiOrder(checkoutId, options) {
3090
+ const json = await executeCartGraphQL(GET_NEXI_ORDER_QUERY, { checkoutId }, options);
3091
+ return json?.data?.Payment?.GetNexiOrder ?? null;
3092
+ }
3093
+ async function updateNexiShipping(variables, options) {
3094
+ const json = await executeCartGraphQL(UPDATE_NEXI_SHIPPING_MUTATION, variables, options);
3095
+ return json?.data?.Payment?.UpdateNexiShipping ?? null;
3096
+ }
3097
+ var NEXI_PAID_STATUSES = /* @__PURE__ */ new Set(["Reserved", "Charged"]);
3098
+ function isNexiPaid(status) {
3099
+ return NEXI_PAID_STATUSES.has(String(status ?? ""));
3100
+ }
3101
+ var NEXI_LANGUAGES = {
3102
+ NO: "nb-NO",
3103
+ SE: "sv-SE",
3104
+ DK: "da-DK",
3105
+ FI: "fi-FI",
3106
+ DE: "de-DE",
3107
+ NL: "nl-NL",
3108
+ PL: "pl-PL",
3109
+ FR: "fr-FR",
3110
+ ES: "es-ES",
3111
+ IT: "it-IT",
3112
+ EE: "ee-EE",
3113
+ LV: "lv-LV",
3114
+ LT: "lt-LT",
3115
+ SK: "sk-SK"
3116
+ };
3117
+ function nexiLanguageFor(countryCode) {
3118
+ return NEXI_LANGUAGES[String(countryCode ?? "").toUpperCase()] ?? "en-GB";
3119
+ }
3120
+ function nexiThemeFrom(theme) {
3121
+ if (!theme) return void 0;
3122
+ const out = {};
3123
+ if (theme.accent) out.primaryColor = theme.accent;
3124
+ if (theme.surface) out.panelColor = theme.surface;
3125
+ if (theme.radiusMd && /^\d+(\.\d+)?px$/.test(theme.radiusMd)) out.buttonRadius = theme.radiusMd;
3126
+ return Object.keys(out).length ? out : void 0;
3127
+ }
3128
+ var scriptLoads = /* @__PURE__ */ new Map();
3129
+ function loadNexiCheckoutJs(url) {
3130
+ const g = globalThis;
3131
+ if (g.Dibs?.Checkout && g.__vioNexiCheckoutJs === url) return Promise.resolve();
3132
+ const inFlight = scriptLoads.get(url);
3133
+ if (inFlight) return inFlight;
3134
+ const load = new Promise((resolve, reject) => {
3135
+ const doc = g.document;
3136
+ if (!doc?.head) {
3137
+ reject(new Error("[Nexi] no document to load checkout.js into"));
3138
+ return;
3139
+ }
3140
+ const script = doc.createElement("script");
3141
+ script.src = url;
3142
+ script.async = true;
3143
+ script.onload = () => {
3144
+ if (g.Dibs?.Checkout) {
3145
+ g.__vioNexiCheckoutJs = url;
3146
+ resolve();
3147
+ } else {
3148
+ scriptLoads.delete(url);
3149
+ reject(new Error("[Nexi] checkout.js loaded but Dibs.Checkout is missing"));
3150
+ }
3151
+ };
3152
+ script.onerror = () => {
3153
+ scriptLoads.delete(url);
3154
+ reject(new Error(`[Nexi] could not load ${url}`));
3155
+ };
3156
+ doc.head.appendChild(script);
3157
+ });
3158
+ scriptLoads.set(url, load);
3159
+ return load;
3160
+ }
3161
+ async function mountNexi(container, order, options = {}) {
3162
+ if (!order?.order_id || !order.checkout_key || !order.checkout_js_url) {
3163
+ throw new Error("[Nexi] order is missing paymentId, checkout_key or checkout_js_url");
3164
+ }
3165
+ await loadNexiCheckoutJs(order.checkout_js_url);
3166
+ if (!container.id) container.id = "vio-nexi-checkout-container";
3167
+ const Dibs = globalThis.Dibs;
3168
+ const checkout = new Dibs.Checkout({
3169
+ checkoutKey: order.checkout_key,
3170
+ paymentId: order.order_id,
3171
+ containerId: container.id,
3172
+ language: options.language ?? "en-GB",
3173
+ ...options.theme ? { theme: options.theme } : {}
3174
+ });
3175
+ return checkout;
3176
+ }
3177
+ var NEXI_PENDING_KEY = "vio.nexi.pending.v1";
3178
+ var NEXI_PENDING_TTL_MS = 48 * 60 * 60 * 1e3;
3179
+ function storage() {
3180
+ try {
3181
+ const s = globalThis.sessionStorage;
3182
+ return s ?? null;
3183
+ } catch {
3184
+ return null;
3185
+ }
3186
+ }
3187
+ function rememberNexiPending(pending2) {
3188
+ try {
3189
+ storage()?.setItem(NEXI_PENDING_KEY, JSON.stringify({ ...pending2, createdAt: Date.now() }));
3190
+ } catch {
3191
+ }
3192
+ }
3193
+ function readNexiPending() {
3194
+ try {
3195
+ const raw = storage()?.getItem(NEXI_PENDING_KEY);
3196
+ if (!raw) return null;
3197
+ const p = JSON.parse(raw);
3198
+ if (!p?.checkoutId || !p?.paymentId) return null;
3199
+ if (Date.now() - Number(p.createdAt ?? 0) > NEXI_PENDING_TTL_MS) {
3200
+ clearNexiPending();
3201
+ return null;
3202
+ }
3203
+ return {
3204
+ checkoutId: String(p.checkoutId),
3205
+ sponsorId: Number(p.sponsorId ?? 0),
3206
+ paymentId: String(p.paymentId),
3207
+ createdAt: Number(p.createdAt ?? 0)
3208
+ };
3209
+ } catch {
3210
+ return null;
3211
+ }
3212
+ }
3213
+ function clearNexiPending() {
3214
+ try {
3215
+ storage()?.removeItem(NEXI_PENDING_KEY);
3216
+ } catch {
3217
+ }
3218
+ }
3219
+ function nexiReturnPaymentId(search) {
3220
+ try {
3221
+ const s = search ?? globalThis.location?.search ?? "";
3222
+ const id = new URLSearchParams(s).get("paymentId");
3223
+ return id && /^[0-9a-f]{32}$/i.test(id) ? id : null;
3224
+ } catch {
3225
+ return null;
3226
+ }
3227
+ }
3228
+
3229
+ // src/core/checkout/payments/adyen.ts
3230
+ var ADYEN_SESSION_FIELDS = `
3231
+ order_id
3232
+ session_id
3233
+ session_data
3234
+ client_key
3235
+ environment
3236
+ country_code
3237
+ shopper_locale
3238
+ purchase_currency
3239
+ amount
3240
+ total_price
3241
+ shipping_name
3242
+ shipping_price
3243
+ expires_at`;
3244
+ var ADYEN_PAYMENT_FIELDS = `
3245
+ order_id
3246
+ status
3247
+ result_code
3248
+ psp_reference
3249
+ payment_method
3250
+ purchase_country
3251
+ purchase_currency
3252
+ total_price
3253
+ shipping_name
3254
+ shipping_price
3255
+ email
3256
+ order_created`;
3257
+ var CREATE_PAYMENT_ADYEN_MUTATION = `
3258
+ mutation CreatePaymentAdyen($checkoutId: String!, $returnUrl: String!, $countryCode: String, $channel: String, $email: String, $client: String) {
3259
+ Payment {
3260
+ CreatePaymentAdyen(checkout_id: $checkoutId, return_url: $returnUrl, country_code: $countryCode, channel: $channel, email: $email, client: $client) {${ADYEN_SESSION_FIELDS}
3261
+ }
3262
+ }
3263
+ }
3264
+ `;
3265
+ var GET_ADYEN_PAYMENT_QUERY = `
3266
+ query GetAdyenPayment($checkoutId: String!, $sessionResult: String, $redirectResult: String) {
3267
+ Payment {
3268
+ GetAdyenPayment(checkout_id: $checkoutId, session_result: $sessionResult, redirect_result: $redirectResult) {${ADYEN_PAYMENT_FIELDS}
3269
+ }
3270
+ }
3271
+ }
3272
+ `;
3273
+ var CONFIRM_ADYEN_PAYMENT_MUTATION = `
3274
+ mutation ConfirmAdyenPayment($checkoutId: String!, $sessionId: String!) {
3275
+ Payment {
3276
+ ConfirmAdyenPayment(checkout_id: $checkoutId, session_id: $sessionId) {
3277
+ ok
3278
+ reason
3279
+ }
3280
+ }
3281
+ }
3282
+ `;
3283
+ async function createPaymentAdyen(variables, options) {
3284
+ const json = await executeCartGraphQL(CREATE_PAYMENT_ADYEN_MUTATION, variables, options);
3285
+ return json?.data?.Payment?.CreatePaymentAdyen ?? null;
3286
+ }
3287
+ async function getAdyenPayment(variables, options) {
3288
+ const json = await executeCartGraphQL(GET_ADYEN_PAYMENT_QUERY, variables, options);
3289
+ return json?.data?.Payment?.GetAdyenPayment ?? null;
3290
+ }
3291
+ async function confirmAdyenPayment(variables, options) {
3292
+ const json = await executeCartGraphQL(CONFIRM_ADYEN_PAYMENT_MUTATION, variables, options);
3293
+ return json?.data?.Payment?.ConfirmAdyenPayment ?? null;
3294
+ }
3295
+ var ADYEN_WEB_VERSION = "6.45.0";
3296
+ var ADYEN_WEB_JS_SRI = "sha384-7KW64UN6T0xZoacpNpRg2KVxMMVwfqwUNTDmalkE1roPBtjOpWe8JuJoUU7OCMvF";
3297
+ var ADYEN_WEB_CSS_SRI = "sha384-A4V58Hp1NTZmMvKn29pBh7249tw4TmfdL2szCtpKvaYfR/ARbXlXRYU20SLp/1VW";
3298
+ var ADYEN_ENVIRONMENTS = ["test", "live", "live-us", "live-au", "live-apse", "live-in"];
3299
+ function adyenAssetUrls(environment) {
3300
+ const env = ADYEN_ENVIRONMENTS.includes(environment) ? environment : "test";
3301
+ const base = `https://checkoutshopper-${env}.cdn.adyen.com/checkoutshopper/sdk/${ADYEN_WEB_VERSION}`;
3302
+ return { js: `${base}/adyen.js`, css: `${base}/adyen.css` };
3303
+ }
3304
+ var assetLoads = /* @__PURE__ */ new Map();
3305
+ function loadAdyenWeb(environment) {
3306
+ const g = globalThis;
3307
+ const { js, css } = adyenAssetUrls(environment);
3308
+ if (g.AdyenWeb?.AdyenCheckout && g.__vioAdyenWeb === js) return Promise.resolve();
3309
+ const inFlight = assetLoads.get(js);
3310
+ if (inFlight) return inFlight;
3311
+ const load = new Promise((resolve, reject) => {
3312
+ const doc = g.document;
3313
+ if (!doc?.head) {
3314
+ reject(new Error("[Adyen] no document to load Adyen Web into"));
3315
+ return;
3316
+ }
3317
+ if (!doc.head.querySelector(`link[data-vio-adyen="${ADYEN_WEB_VERSION}"]`)) {
3318
+ const link = doc.createElement("link");
3319
+ link.rel = "stylesheet";
3320
+ link.href = css;
3321
+ link.integrity = ADYEN_WEB_CSS_SRI;
3322
+ link.crossOrigin = "anonymous";
3323
+ link.setAttribute("data-vio-adyen", ADYEN_WEB_VERSION);
3324
+ doc.head.appendChild(link);
3325
+ }
3326
+ const script = doc.createElement("script");
3327
+ script.src = js;
3328
+ script.async = true;
3329
+ script.integrity = ADYEN_WEB_JS_SRI;
3330
+ script.crossOrigin = "anonymous";
3331
+ script.onload = () => {
3332
+ if (g.AdyenWeb?.AdyenCheckout && g.AdyenWeb?.Dropin) {
3333
+ g.__vioAdyenWeb = js;
3334
+ resolve();
3335
+ } else {
3336
+ reject(new Error("[Adyen] adyen.js loaded but AdyenWeb.AdyenCheckout is missing"));
3337
+ }
3338
+ };
3339
+ script.onerror = () => {
3340
+ reject(new Error(`[Adyen] could not load ${js}`));
3341
+ };
3342
+ doc.head.appendChild(script);
3343
+ }).finally(() => {
3344
+ assetLoads.delete(js);
3345
+ });
3346
+ assetLoads.set(js, load);
3347
+ return load;
3348
+ }
3349
+ var AdyenOriginError = class extends Error {
3350
+ constructor(origin) {
3351
+ super(
3352
+ `[Adyen] ${origin} is not an allowed origin of this Adyen client key \u2014 add it under Developers > API credentials > Client settings > Allowed origins.`
3353
+ );
3354
+ this.origin = origin;
3355
+ this.name = "AdyenOriginError";
3356
+ }
3357
+ };
3358
+ var looksLikeOriginProblem = (err) => /origin|client\s?key|14_0381|401|403/i.test(String(err?.message ?? err ?? ""));
3359
+ async function mountAdyen(container, session, handlers, options = {}) {
3360
+ if (!session?.session_id || !session.session_data || !session.client_key) {
3361
+ throw new Error("[Adyen] session is missing session_id, session_data or client_key");
3362
+ }
3363
+ await loadAdyenWeb(session.environment);
3364
+ const { AdyenCheckout, Dropin } = globalThis.AdyenWeb;
3365
+ if (options.theme?.accent) {
3366
+ container.style.setProperty("--adyen-sdk-color-background-always-dark", options.theme.accent);
3367
+ }
3368
+ if (options.theme?.radiusMd && /^\d+(\.\d+)?px$/.test(options.theme.radiusMd)) {
3369
+ container.style.setProperty("--adyen-sdk-border-radius-m", options.theme.radiusMd);
3370
+ }
3371
+ let paying = false;
3372
+ let checkout;
3373
+ try {
3374
+ checkout = await AdyenCheckout({
3375
+ session: { id: session.session_id, sessionData: session.session_data },
3376
+ clientKey: session.client_key,
3377
+ environment: session.environment,
3378
+ locale: session.shopper_locale,
3379
+ countryCode: session.country_code,
3380
+ amount: { value: session.amount, currency: session.purchase_currency },
3381
+ beforeSubmit: async (data, _component, actions) => {
3382
+ if (paying) {
3383
+ actions.reject();
3384
+ return;
3385
+ }
3386
+ paying = true;
3387
+ let proceed = false;
3388
+ try {
3389
+ proceed = await handlers.onBeforePay();
3390
+ } catch {
3391
+ proceed = false;
3392
+ }
3393
+ if (proceed) {
3394
+ actions.resolve(data);
3395
+ } else {
3396
+ paying = false;
3397
+ actions.reject();
3398
+ }
3399
+ },
3400
+ onPaymentCompleted: (result) => {
3401
+ paying = false;
3402
+ handlers.onCompleted(result ?? {});
3403
+ },
3404
+ onPaymentFailed: (result) => {
3405
+ paying = false;
3406
+ handlers.onFailed(result ?? {});
3407
+ },
3408
+ onError: (error) => {
3409
+ paying = false;
3410
+ handlers.onError?.(error);
3411
+ }
3412
+ });
3413
+ } catch (err) {
3414
+ if (looksLikeOriginProblem(err)) {
3415
+ throw new AdyenOriginError(globalThis.location?.origin ?? "this page");
3416
+ }
3417
+ throw err;
3418
+ }
3419
+ const dropin = new Dropin(checkout, {
3420
+ // Vio's own confirmation follows; Adyen's success animation would sit
3421
+ // between the payment and it.
3422
+ disableFinalAnimation: true,
3423
+ instantPaymentTypes: ["applepay", "googlepay"]
3424
+ }).mount(container);
3425
+ return {
3426
+ sessionId: session.session_id,
3427
+ unmount: () => {
3428
+ try {
3429
+ dropin.unmount();
3430
+ } catch {
3431
+ }
3432
+ }
3433
+ };
3434
+ }
3435
+ var ADYEN_RETURN_METHOD = "adyen";
3436
+ function adyenReturnParams(search = globalThis?.location?.search ?? "") {
3437
+ try {
3438
+ const params = new URLSearchParams(search);
3439
+ const redirectResult = params.get("redirectResult");
3440
+ if (!redirectResult) return null;
3441
+ return { redirectResult, sessionId: params.get("sessionId") };
3442
+ } catch {
3443
+ return null;
3444
+ }
3445
+ }
3446
+ var ADYEN_RETURN_QUERY_KEYS = [
3447
+ "vio_payment",
3448
+ "vio_method",
3449
+ "vio_sponsor",
3450
+ "checkout_id",
3451
+ "sessionId",
3452
+ "redirectResult"
3453
+ ];
3454
+ function adyenReturnOutcome(payment) {
3455
+ switch (payment?.status) {
3456
+ case "authorised":
3457
+ return "paid";
3458
+ case "refused":
3459
+ case "cancelled":
3460
+ case "error":
3461
+ return "failed";
3462
+ // pending (Swish, Trustly…) and anything unrecognised: the webhook
3463
+ // decides, and offering to pay again is the one thing not to do.
3464
+ default:
3465
+ return "pending";
3466
+ }
3467
+ }
3468
+
2808
3469
  // src/core/checkout/checkout-manager.ts
2809
3470
  function describeError(err) {
2810
3471
  if (err instanceof Error) return err.message;
@@ -2993,8 +3654,33 @@ function buildCheckoutAddress(address) {
2993
3654
  country_code
2994
3655
  };
2995
3656
  }
3657
+ function adyenReturnUrl(params) {
3658
+ let page = null;
3659
+ if (typeof window !== "undefined" && window.location?.href) {
3660
+ try {
3661
+ page = new URL(window.location.href);
3662
+ ADYEN_RETURN_QUERY_KEYS.forEach((k) => page?.searchParams.delete(k));
3663
+ } catch {
3664
+ page = null;
3665
+ }
3666
+ }
3667
+ const local = page?.protocol === "http:" && /^(localhost|127\.0\.0\.1)$/.test(page.hostname);
3668
+ const build = (base) => {
3669
+ if (base && local) {
3670
+ const url = new URL(base.toString());
3671
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)));
3672
+ return url.toString();
3673
+ }
3674
+ return resolveHttpsReturnUrl(base ? base.toString() : null, params);
3675
+ };
3676
+ const full = build(page);
3677
+ if (full.length <= 1024 || !page) return full;
3678
+ const bare = new URL(page.toString());
3679
+ bare.search = "";
3680
+ return build(bare);
3681
+ }
2996
3682
  function resolveHttpsReturnUrl(overrideUrl, params = null) {
2997
- let url = void 0;
3683
+ let url = overrideUrl ?? void 0;
2998
3684
  if (!url && typeof window !== "undefined" && window.location) {
2999
3685
  url = window.location.href;
3000
3686
  }
@@ -3032,23 +3718,45 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3032
3718
  super();
3033
3719
  this.cartManager = cartManager;
3034
3720
  this.state = null;
3721
+ /** Counts `open()` calls — see CheckoutState.session. */
3722
+ this.sessionSeq = 0;
3035
3723
  this.applePayAvailableCache = null;
3036
3724
  /** Gateway hints learned from CreatePaymentApplePay (per session). The
3037
3725
  * backend's gateway_merchant_id may be a publishable key (pk_…) or a Stripe
3038
3726
  * Connect account (acct_…) — stored defensively by prefix. */
3039
3727
  this.applePayPublishableKey = null;
3040
3728
  this.applePayConnectedAccount = null;
3729
+ /** Stripe Connect account of the channel, from its Stripe config. */
3730
+ this.stripeConnectAccount = null;
3041
3731
  this.klarnaAvailableCache = null;
3042
3732
  /** Live q1 listeners while a Qliro widget is mounted. */
3043
3733
  this.qliroController = null;
3734
+ /** Live Dibs.Checkout handle while a Nexi widget is mounted. */
3735
+ this.nexiHandle = null;
3736
+ /** Live Drop-in while an Adyen session is mounted. */
3737
+ this.adyenHandle = null;
3738
+ this.stripeHandle = null;
3739
+ /**
3740
+ * The rate the shopper wants. Before an address it is only remembered (the
3741
+ * suggested one, or their pick); every re-pricing asks for it, and the
3742
+ * backend charges it if it reaches the address.
3743
+ */
3744
+ this.nexiPreferredShipping = null;
3745
+ /** The rates offered before an address was typed. */
3746
+ this.nexiShippingPreview = null;
3747
+ /** What the last `address-changed` was priced with — a picked rate re-prices the same address. */
3748
+ this.nexiShippingContext = null;
3044
3749
  this.klarnaOrderInFlight = false;
3045
3750
  /** Last backend shippings fetched (per-supplier), for UI reuse. */
3046
3751
  this.lastFetchedShippings = [];
3752
+ /** See `lastShippingsResolved`. */
3753
+ this.shippingsResolved = false;
3047
3754
  /** Per-sponsor payment methods cache + in-flight dedupe. */
3048
3755
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3049
3756
  this.paymentMethodsCache = /* @__PURE__ */ new Map();
3050
3757
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3051
3758
  this.paymentMethodsPromises = /* @__PURE__ */ new Map();
3759
+ this.kustomController = null;
3052
3760
  preloadStripeJs();
3053
3761
  }
3054
3762
  open(sponsorId) {
@@ -3058,6 +3766,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3058
3766
  }
3059
3767
  this.state = {
3060
3768
  sponsorId,
3769
+ session: ++this.sessionSeq,
3061
3770
  subtotal: this.cartManager.subtotalForSponsor(sponsorId),
3062
3771
  // Page-level currency wins over whatever the persisted cart carries —
3063
3772
  // the host may have switched currency since the cart was created.
@@ -3135,6 +3844,20 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3135
3844
  const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
3136
3845
  return getCheckout(checkoutId, opts);
3137
3846
  }
3847
+ /**
3848
+ * The Vio order number (#4497) a checkout became, or null: not born yet,
3849
+ * or an API that does not say it. Never throws — it is a line on a
3850
+ * receipt, and a receipt does not fail for it.
3851
+ */
3852
+ async getOrderNumber(checkoutId, sponsorId) {
3853
+ if (!checkoutId) return null;
3854
+ try {
3855
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
3856
+ return await getCheckoutOrderNumber(checkoutId, opts);
3857
+ } catch {
3858
+ return null;
3859
+ }
3860
+ }
3138
3861
  /**
3139
3862
  * Payment methods enabled for the sponsor (backend-configured). Memoised per
3140
3863
  * sponsor with in-flight dedupe — several UI components ask on every open.
@@ -3174,6 +3897,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3174
3897
  * this at all.
3175
3898
  */
3176
3899
  async fetchAvailableShippings(sponsorId) {
3900
+ this.shippingsResolved = false;
3177
3901
  try {
3178
3902
  const spId = sponsorId ?? this.state?.sponsorId;
3179
3903
  if (!spId) return [];
@@ -3186,6 +3910,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3186
3910
  if (!cartId) return [];
3187
3911
  const opts = await getCartGraphQLOptions(spId);
3188
3912
  const supplierGroups = await getLineItemsBySupplier(cartId, opts);
3913
+ this.shippingsResolved = Array.isArray(supplierGroups);
3189
3914
  const firstGroup = Array.isArray(supplierGroups) ? supplierGroups[0] : null;
3190
3915
  const shippings = firstGroup?.available_shippings ?? [];
3191
3916
  if (Array.isArray(shippings) && shippings.length > 0) {
@@ -3208,6 +3933,10 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3208
3933
  preselected: idx === 0
3209
3934
  };
3210
3935
  });
3936
+ mapped.sort((a, b) => a.price - b.price);
3937
+ mapped.forEach((m, idx) => {
3938
+ m.preselected = idx === 0;
3939
+ });
3211
3940
  this.lastFetchedShippings = mapped;
3212
3941
  return mapped;
3213
3942
  }
@@ -3218,6 +3947,15 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3218
3947
  }
3219
3948
  return [];
3220
3949
  }
3950
+ /**
3951
+ * Whether the last `fetchAvailableShippings` got an answer from the backend,
3952
+ * possibly an empty one, rather than failing or finding no cart. The two
3953
+ * return the same empty list, and only an answer means the cart has
3954
+ * nothing it can ship with.
3955
+ */
3956
+ get lastShippingsResolved() {
3957
+ return this.shippingsResolved;
3958
+ }
3221
3959
  /** Set the chosen shipping per supplier on the backend cart. */
3222
3960
  async updateShippingsBySupplier(data, sponsorId) {
3223
3961
  const spId = sponsorId ?? this.state?.sponsorId;
@@ -3260,12 +3998,13 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3260
3998
  const addr = typeof formData === "object" && formData !== null ? formData : this.state?.address;
3261
3999
  const addressObj = !isVipps ? buildCheckoutAddress(addr) : null;
3262
4000
  const emailVal = (typeof formData === "string" ? formData : formData?.email) || this.state?.address?.email || "";
3263
- if (!emailVal || !emailVal.includes("@")) {
4001
+ const hasEmail = !!emailVal && emailVal.includes("@");
4002
+ if (!hasEmail && !isVipps) {
3264
4003
  throw new Error("[CheckoutManager] email is required to start the payment");
3265
4004
  }
3266
4005
  const updateVars = {
3267
4006
  checkoutId,
3268
- email: emailVal,
4007
+ ...hasEmail ? { email: emailVal } : {},
3269
4008
  buyerAcceptsPurchaseConditions: true,
3270
4009
  buyerAcceptsTermsConditions: true,
3271
4010
  ...extraUpdateVars
@@ -3337,10 +4076,21 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3337
4076
  throw new Error("Stripe payment link generation failed: missing checkout_url");
3338
4077
  }
3339
4078
  /**
3340
- * Vipps payment link: same flow as Stripe — CreatePaymentVipps → redirect to
3341
- * payment_url.
4079
+ * Vipps: create the payment via Vio Commerce and redirect to Vipps'
4080
+ * landing page (on a phone it opens the app; on a desktop it offers to
4081
+ * send the payment to the phone).
4082
+ *
4083
+ * `express: true` asks for Vipps Express — address and delivery chosen
4084
+ * inside the app, the email handed back with the payment — which is what
4085
+ * the Vipps buttons on the product and the cart ask for. When the cart
4086
+ * cannot have it (several suppliers, digital goods, no rate for the
4087
+ * country) the backend creates NO payment and answers `express: false`
4088
+ * with the reason; this resolves with that answer, without redirecting,
4089
+ * so the caller can open the ordinary checkout instead. Without the flag
4090
+ * the backend decides: Express while the cart has no delivery yet, a
4091
+ * plain payment once the shopper chose one in our form.
3342
4092
  */
3343
- async startVippsPayment(sponsorId, formData) {
4093
+ async startVippsPayment(sponsorId, formData, options = {}) {
3344
4094
  const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
3345
4095
  sponsorId,
3346
4096
  formData,
@@ -3353,15 +4103,31 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3353
4103
  checkout_id: checkoutId
3354
4104
  });
3355
4105
  const vippsRes = await createPaymentVipps(
3356
- { checkoutId, email: emailVal, returnUrl },
4106
+ {
4107
+ checkoutId,
4108
+ ...emailVal && emailVal.includes("@") ? { email: emailVal } : {},
4109
+ returnUrl,
4110
+ ...options.express !== void 0 ? { express: options.express } : {}
4111
+ },
3357
4112
  opts
3358
4113
  );
3359
4114
  if (vippsRes?.payment_url) {
3360
4115
  _CheckoutManager.redirectTo(vippsRes.payment_url);
3361
4116
  return vippsRes;
3362
4117
  }
4118
+ if (options.express === true && vippsRes?.express === false) {
4119
+ return vippsRes;
4120
+ }
3363
4121
  throw new Error("Vipps payment link generation failed: missing payment_url");
3364
4122
  }
4123
+ /**
4124
+ * The Vipps Express button: the shopper taps it and the app opens with
4125
+ * address and delivery already there. Resolves with `{ express: false,
4126
+ * reason }` — and nothing redirected — when this cart cannot have it.
4127
+ */
4128
+ async startVippsExpress(sponsorId) {
4129
+ return this.startVippsPayment(sponsorId, void 0, { express: true });
4130
+ }
3365
4131
  /** Vipps' own payment state for the checkout — see getVippsStatus in
3366
4132
  * cart-queries.ts for why this exists instead of trusting GetCheckout. */
3367
4133
  async getVippsStatus(checkoutId, sponsorId) {
@@ -3373,7 +4139,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3373
4139
  * and render its html_snippet into `container`. The widget collects
3374
4140
  * address, shipping and payment by itself; the SDK renders nothing else.
3375
4141
  */
3376
- async mountKustomCheckout(container, sponsorId) {
4142
+ async mountKustomCheckout(container, sponsorId, handlers = {}) {
3377
4143
  const spId = sponsorId ?? this.state?.sponsorId;
3378
4144
  if (!spId) throw new Error("[CheckoutManager] no sponsor for Kustom");
3379
4145
  const cart = this.cartManager.getCart(spId);
@@ -3400,16 +4166,42 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3400
4166
  checkoutId,
3401
4167
  countryCode: getGlobalCountryCode(),
3402
4168
  href,
3403
- email: this.state?.address?.email || void 0
4169
+ email: this.state?.address?.email || void 0,
4170
+ client: `web-sdk ${SDK_VERSION}`
3404
4171
  },
3405
4172
  opts
3406
4173
  );
3407
4174
  if (!order?.html_snippet) {
3408
4175
  throw new Error("Kustom order creation failed: missing html_snippet");
3409
4176
  }
4177
+ this.destroyKustomListeners();
3410
4178
  renderKustomSnippet(container, order.html_snippet);
4179
+ this.kustomController = installKustomListeners(handlers);
3411
4180
  return order;
3412
4181
  }
4182
+ /** Disarm the widget's listeners with the widget they belong to. */
4183
+ destroyKustomListeners() {
4184
+ this.kustomController?.destroy();
4185
+ this.kustomController = null;
4186
+ }
4187
+ /**
4188
+ * The cart changed under an open Kustom widget: lock it, push the cart
4189
+ * into the SAME order, and let the widget refresh itself. Kustom's own
4190
+ * rule — never a new order for a cart change. The widget auto-resumes
4191
+ * after 10 s if the sync never comes back.
4192
+ */
4193
+ async syncKustomOrder(sponsorId) {
4194
+ const spId = sponsorId ?? this.state?.sponsorId;
4195
+ const checkoutId = this.state?.checkoutId;
4196
+ if (!spId || !checkoutId) return null;
4197
+ const opts = await getCartGraphQLOptions(spId);
4198
+ suspendKustom();
4199
+ try {
4200
+ return await syncPaymentKustom(checkoutId, opts);
4201
+ } finally {
4202
+ resumeKustom();
4203
+ }
4204
+ }
3413
4205
  /** Re-read a Kustom order — used on the confirmation return trip. */
3414
4206
  async getKustomOrder(orderId, sponsorId) {
3415
4207
  const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
@@ -3560,6 +4352,344 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3560
4352
  const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
3561
4353
  return getWalleyOrder(checkoutId, opts);
3562
4354
  }
4355
+ /**
4356
+ * Mount the Nexi embedded checkout — Nexi's own JS SDK, not a snippet.
4357
+ *
4358
+ * `onCompleted` fires on Nexi's `payment-completed` (no redirect: Nexi
4359
+ * renders no receipt, the component shows Vio's confirmation). Shipping is
4360
+ * Vio's: Nexi holds its pay button until `address-changed` has been
4361
+ * re-priced through `UpdateNexiShipping` — the widget is frozen meanwhile
4362
+ * and `onShipping` reports the outcome (NO_SHIPPING keeps it held).
4363
+ *
4364
+ * A session interrupted by a third-party redirect inside Nexi (Vipps,
4365
+ * Swish, MobilePay) is remembered in sessionStorage and resumed on the
4366
+ * same paymentId instead of creating a new payment — only when `resume`
4367
+ * names it. Every other opening creates a payment for the cart as it is.
4368
+ */
4369
+ async mountNexiCheckout(container, sponsorId, handlers = {}, resume) {
4370
+ const spId = sponsorId ?? this.state?.sponsorId;
4371
+ if (!spId) throw new Error("[CheckoutManager] no sponsor for Nexi");
4372
+ const cart = this.cartManager.getCart(spId);
4373
+ let cartId = cart?.cartId;
4374
+ if (!cartId) cartId = await this.cartManager.ensureCartId(spId);
4375
+ if (!cartId) {
4376
+ throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
4377
+ }
4378
+ const opts = await getCartGraphQLOptions(spId);
4379
+ const stored = resume ? readNexiPending() : null;
4380
+ const pending2 = stored && stored.paymentId === resume?.paymentId ? stored : null;
4381
+ let checkoutId = this.state?.checkoutId;
4382
+ if (!checkoutId && pending2 && pending2.sponsorId === spId) {
4383
+ checkoutId = pending2.checkoutId;
4384
+ if (this.state) this.state = { ...this.state, checkoutId };
4385
+ }
4386
+ if (!checkoutId) {
4387
+ const checkoutRes = await this.createCheckout(spId);
4388
+ checkoutId = checkoutRes?.id;
4389
+ if (checkoutId && this.state) {
4390
+ this.state = { ...this.state, checkoutId };
4391
+ }
4392
+ }
4393
+ if (!checkoutId) {
4394
+ throw new Error("[CheckoutManager] no backend checkout for the Nexi payment");
4395
+ }
4396
+ const ownedCheckoutId = checkoutId;
4397
+ let order = null;
4398
+ if (pending2 && pending2.checkoutId === ownedCheckoutId) {
4399
+ try {
4400
+ order = await getNexiOrder(ownedCheckoutId, opts);
4401
+ } catch {
4402
+ order = null;
4403
+ }
4404
+ if (order && isNexiPaid(order.status)) {
4405
+ clearNexiPending();
4406
+ handlers.onCompleted?.(order);
4407
+ return order;
4408
+ }
4409
+ if (order && (order.status !== "Created" || order.order_id !== pending2.paymentId)) {
4410
+ order = null;
4411
+ }
4412
+ }
4413
+ if (!order) {
4414
+ const created = await createPaymentNexi(
4415
+ {
4416
+ checkoutId: ownedCheckoutId,
4417
+ countryCode: getGlobalCountryCode(),
4418
+ href: kustomCleanHref(),
4419
+ email: this.state?.address?.email || void 0
4420
+ },
4421
+ opts
4422
+ );
4423
+ if (!created?.order_id || !created.checkout_key) {
4424
+ throw new Error("Nexi payment creation failed: missing paymentId/checkout key");
4425
+ }
4426
+ order = created;
4427
+ rememberNexiPending({ checkoutId: ownedCheckoutId, sponsorId: spId, paymentId: order.order_id });
4428
+ }
4429
+ this.destroyNexi();
4430
+ const handle = await mountNexi(container, order, {
4431
+ language: nexiLanguageFor(getGlobalCountryCode()),
4432
+ theme: nexiThemeFrom(readVioTheme())
4433
+ });
4434
+ this.nexiHandle = handle;
4435
+ const mounted = order;
4436
+ const carried = order.shipping;
4437
+ this.nexiPreferredShipping = carried?.shipping_id ?? null;
4438
+ this.nexiShippingPreview = null;
4439
+ if (carried?.ok && carried.country) {
4440
+ this.nexiShippingContext = {
4441
+ handle,
4442
+ checkoutId: ownedCheckoutId,
4443
+ address: { countryCode: carried.country, postalCode: carried.postal_code },
4444
+ fromShopper: carried.address_source !== "market",
4445
+ opts,
4446
+ onShipping: handlers.onShipping
4447
+ };
4448
+ handlers.onShipping?.(carried);
4449
+ } else if (carried?.reason === "AWAITING_ADDRESS") {
4450
+ this.nexiShippingPreview = carried;
4451
+ this.nexiShippingContext = {
4452
+ handle,
4453
+ checkoutId: ownedCheckoutId,
4454
+ address: null,
4455
+ opts,
4456
+ onShipping: handlers.onShipping
4457
+ };
4458
+ handlers.onShipping?.(carried);
4459
+ }
4460
+ const onAddress = (address) => {
4461
+ this.nexiShippingContext = {
4462
+ handle,
4463
+ checkoutId: ownedCheckoutId,
4464
+ address,
4465
+ fromShopper: true,
4466
+ opts,
4467
+ onShipping: handlers.onShipping
4468
+ };
4469
+ void this.repriceNexiShipping(
4470
+ handle,
4471
+ ownedCheckoutId,
4472
+ address,
4473
+ opts,
4474
+ handlers.onShipping,
4475
+ this.nexiPreferredShipping ?? void 0
4476
+ );
4477
+ };
4478
+ handle.on("address-changed", onAddress);
4479
+ handle.on("applepay-contact-updated", onAddress);
4480
+ handle.on("pay-initialized", () => {
4481
+ void this.finalizeNexiPayment(handle, ownedCheckoutId, opts, handlers.onShipping);
4482
+ });
4483
+ handle.on("payment-completed", (result) => {
4484
+ clearNexiPending();
4485
+ handlers.onCompleted?.({
4486
+ ...mounted,
4487
+ order_id: String(result?.paymentId ?? mounted.order_id),
4488
+ status: "Reserved"
4489
+ });
4490
+ });
4491
+ return order;
4492
+ }
4493
+ /**
4494
+ * The shopper picked another rate from the `options` Nexi's last
4495
+ * re-pricing returned: charge that one instead. Nexi has no shipping picker,
4496
+ * so the checkout shows the rates and calls this. A no-op until an address
4497
+ * has been typed in the widget.
4498
+ */
4499
+ async pickNexiShipping(shippingId) {
4500
+ const ctx = this.nexiShippingContext;
4501
+ if (!ctx || ctx.handle !== this.nexiHandle) return;
4502
+ this.nexiPreferredShipping = shippingId;
4503
+ if (!ctx.address) {
4504
+ const preview = this.nexiShippingPreview;
4505
+ const picked = preview?.options?.find((o) => o.id === shippingId);
4506
+ if (preview && picked) {
4507
+ this.nexiShippingPreview = {
4508
+ ...preview,
4509
+ shipping_id: picked.id,
4510
+ shipping_name: picked.name,
4511
+ shipping_price: picked.price
4512
+ };
4513
+ ctx.onShipping?.(this.nexiShippingPreview);
4514
+ }
4515
+ return;
4516
+ }
4517
+ await this.repriceNexiShipping(ctx.handle, ctx.checkoutId, ctx.address, ctx.opts, ctx.onShipping, shippingId);
4518
+ }
4519
+ /**
4520
+ * `pay-initialized` → confirm the shipping for the address Nexi will ship
4521
+ * to → `payment-order-finalized`.
4522
+ *
4523
+ * The payment already carries a shipping (set at creation, re-priced on
4524
+ * every address and pick), so normally nothing changes and Nexi charges.
4525
+ * When the shopper's address was never announced — Nexi fills it in for a
4526
+ * shopper it recognises — the backend reads it from the payment. If that
4527
+ * changes the amount, the payment is STOPPED and the widget reloaded: an
4528
+ * amount that changes while Nexi charges is what failed on 2026-09-16/17
4529
+ * (payments 085277846ae44f7498ae94e112435d63, f6116e6d68294a88927d35b05d907f10).
4530
+ */
4531
+ async finalizeNexiPayment(handle, checkoutId, opts, onShipping) {
4532
+ const ctx = this.nexiShippingContext?.handle === handle ? this.nexiShippingContext : null;
4533
+ const address = ctx?.fromShopper ? ctx.address : null;
4534
+ let proceed = false;
4535
+ let reload = false;
4536
+ try {
4537
+ const result = await updateNexiShipping(
4538
+ {
4539
+ checkoutId,
4540
+ countryCode: String(address?.countryCode ?? address?.country ?? ""),
4541
+ postalCode: address?.postalCode ? String(address.postalCode) : void 0,
4542
+ shippingId: this.nexiPreferredShipping ?? void 0
4543
+ },
4544
+ opts
4545
+ );
4546
+ if (result?.ok) {
4547
+ if (result.shipping_id) this.nexiPreferredShipping = result.shipping_id;
4548
+ if (ctx && result.country) {
4549
+ ctx.address = { countryCode: result.country, postalCode: result.postal_code };
4550
+ ctx.fromShopper = true;
4551
+ }
4552
+ reload = result.changed === true;
4553
+ proceed = !reload;
4554
+ }
4555
+ onShipping?.(result ? { ...result, ...reload ? { repriced_at_pay: true } : {} } : result);
4556
+ } catch (err) {
4557
+ onShipping?.(null, err);
4558
+ }
4559
+ try {
4560
+ handle.send?.("payment-order-finalized", proceed);
4561
+ } catch {
4562
+ }
4563
+ if (reload) {
4564
+ try {
4565
+ handle.thawCheckout();
4566
+ } catch {
4567
+ }
4568
+ }
4569
+ }
4570
+ /** address-changed → freeze → UpdateNexiShipping → thaw. */
4571
+ async repriceNexiShipping(handle, checkoutId, address, opts, onShipping, shippingId) {
4572
+ const countryCode = String(address?.countryCode ?? address?.country ?? "");
4573
+ if (!countryCode) return;
4574
+ try {
4575
+ handle.freezeCheckout();
4576
+ } catch {
4577
+ }
4578
+ try {
4579
+ const result = await updateNexiShipping(
4580
+ {
4581
+ checkoutId,
4582
+ countryCode,
4583
+ postalCode: address?.postalCode ? String(address.postalCode) : void 0,
4584
+ shippingId
4585
+ },
4586
+ opts
4587
+ );
4588
+ if (result?.ok && result.shipping_id) this.nexiPreferredShipping = result.shipping_id;
4589
+ onShipping?.(result);
4590
+ } catch (err) {
4591
+ onShipping?.(null, err);
4592
+ } finally {
4593
+ try {
4594
+ handle.thawCheckout();
4595
+ } catch {
4596
+ }
4597
+ }
4598
+ }
4599
+ /** Release the Dibs.Checkout listeners with the widget they belong to. */
4600
+ destroyNexi() {
4601
+ try {
4602
+ this.nexiHandle?.cleanup();
4603
+ } catch {
4604
+ }
4605
+ this.nexiHandle = null;
4606
+ this.nexiShippingContext = null;
4607
+ this.nexiPreferredShipping = null;
4608
+ this.nexiShippingPreview = null;
4609
+ }
4610
+ /** Re-read the Nexi payment owned by a checkout. */
4611
+ async getNexiOrder(checkoutId, sponsorId) {
4612
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
4613
+ return getNexiOrder(checkoutId, opts);
4614
+ }
4615
+ /**
4616
+ * Adyen: FORM-FIRST. Saves the shopper's form and shipping pick on the
4617
+ * checkout (Adyen collects neither), creates a session with that final
4618
+ * amount and mounts Adyen's Drop-in on it. A session is never updated —
4619
+ * after ANY change the caller destroys this mount and calls again.
4620
+ *
4621
+ * At "Pay" the backend only confirms that the session still pays for the
4622
+ * checkout; when it does not, Adyen is stopped and `onReplaced` tells the
4623
+ * caller to remount. Completion is asked of Adyen through the backend
4624
+ * (`GetAdyenPayment`) — the browser's own result is never the answer.
4625
+ */
4626
+ async mountAdyenCheckout(container, sponsorId, formData, callbacks) {
4627
+ this.destroyAdyen();
4628
+ const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
4629
+ sponsorId,
4630
+ formData,
4631
+ { paymentMethod: "Adyen" }
4632
+ );
4633
+ const session = await createPaymentAdyen(
4634
+ {
4635
+ checkoutId,
4636
+ returnUrl: adyenReturnUrl({
4637
+ vio_payment: "return",
4638
+ vio_method: ADYEN_RETURN_METHOD,
4639
+ vio_sponsor: spId,
4640
+ checkout_id: checkoutId
4641
+ }),
4642
+ countryCode: getGlobalCountryCode(),
4643
+ channel: "Web",
4644
+ email: emailVal,
4645
+ client: `web-sdk ${SDK_VERSION}`
4646
+ },
4647
+ opts
4648
+ );
4649
+ if (!session?.session_id) {
4650
+ throw new Error("[CheckoutManager] Adyen session was not created");
4651
+ }
4652
+ const theme = readVioTheme();
4653
+ this.adyenHandle = await mountAdyen(
4654
+ container,
4655
+ session,
4656
+ {
4657
+ onBeforePay: async () => {
4658
+ const verdict = await confirmAdyenPayment(
4659
+ { checkoutId, sessionId: session.session_id },
4660
+ opts
4661
+ );
4662
+ if (verdict?.ok === true) return true;
4663
+ callbacks.onReplaced(String(verdict?.reason ?? "UNKNOWN"));
4664
+ return false;
4665
+ },
4666
+ onCompleted: (result) => {
4667
+ void getAdyenPayment({ checkoutId, sessionResult: result?.sessionResult }, opts).catch(() => null).then((payment) => callbacks.onCompleted(payment, session));
4668
+ },
4669
+ onFailed: (result) => callbacks.onFailed(result, session),
4670
+ onError: (error) => callbacks.onError?.(error)
4671
+ },
4672
+ { theme: { accent: theme?.accent, radiusMd: theme?.radiusMd } }
4673
+ );
4674
+ return session;
4675
+ }
4676
+ /** Unmount Drop-in. Safe to call when nothing is mounted. */
4677
+ destroyAdyen() {
4678
+ try {
4679
+ this.adyenHandle?.unmount();
4680
+ } catch {
4681
+ }
4682
+ this.adyenHandle = null;
4683
+ }
4684
+ /**
4685
+ * Outcome of the Adyen session a checkout owns. With `redirectResult` (the
4686
+ * return from Vipps, Klarna, Trustly or a redirect-3DS) the redirect is
4687
+ * finalised on the server.
4688
+ */
4689
+ async getAdyenPayment(checkoutId, proof = {}, sponsorId) {
4690
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
4691
+ return getAdyenPayment({ checkoutId, ...proof }, opts);
4692
+ }
3563
4693
  /* eslint-enable @typescript-eslint/no-explicit-any */
3564
4694
  setAddress(address) {
3565
4695
  if (!this.state) return;
@@ -3577,6 +4707,83 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3577
4707
  );
3578
4708
  }
3579
4709
  // MARK: - Apple Pay (Stripe Payment Request)
4710
+ /**
4711
+ * Which Stripe flow this channel offers: `native` pays on our own page,
4712
+ * `link` sends the shopper to a page hosted by Stripe. The channel's two
4713
+ * toggles decide it and the answer travels in the method's config
4714
+ * (api-microservice, `stripe-offer.ts`). A channel that predates that
4715
+ * field answers `link`, which is what every client did before.
4716
+ */
4717
+ /**
4718
+ * Does this channel's Vipps offer Express — the buy-now buttons that open
4719
+ * the app with address and delivery? Vio Commerce announces it as
4720
+ * `config.express` on the Vipps method, the way Stripe announces `mode`;
4721
+ * the seller switches it on its Vipps row. Unknown means yes: Express is
4722
+ * the default, and the backend refuses it with a reason if it cannot.
4723
+ */
4724
+ async getVippsExpressEnabled(sponsorId) {
4725
+ try {
4726
+ const methods = await this.getAvailablePaymentMethods(sponsorId);
4727
+ const vipps = methods?.find((m) => String(m?.name ?? "").toLowerCase() === "vipps");
4728
+ const express = vipps?.config?.find((c) => c?.name === "express")?.value;
4729
+ return express !== "false";
4730
+ } catch {
4731
+ return true;
4732
+ }
4733
+ }
4734
+ async getStripeMode(sponsorId) {
4735
+ try {
4736
+ const methods = await this.getAvailablePaymentMethods(sponsorId);
4737
+ const stripe = methods?.find((m) => String(m?.name ?? "").toLowerCase() === "stripe");
4738
+ const mode = stripe?.config?.find((c) => c?.name === "mode")?.value;
4739
+ return mode === "native" ? "native" : "link";
4740
+ } catch {
4741
+ return "link";
4742
+ }
4743
+ }
4744
+ /**
4745
+ * Stripe's Payment Element, mounted on our own page for a checkout that is
4746
+ * ready to be paid. Creating it mints a PaymentIntent, so it happens when
4747
+ * the shopper asks for the payment step, never while they type.
4748
+ *
4749
+ * The order is not born here: `confirm()` only says what Stripe told the
4750
+ * browser. The `payment_intent.succeeded` webhook creates it server-side,
4751
+ * and the caller then reads the checkout to see it land.
4752
+ */
4753
+ async mountStripeCheckout(container, sponsorId, formData, callbacks) {
4754
+ this.destroyStripe();
4755
+ const { spId, checkoutId, opts } = await this.prepareRedirectPayment(sponsorId, formData, {
4756
+ paymentMethod: "Stripe"
4757
+ });
4758
+ const minted = await createStripeIntent(checkoutId, opts);
4759
+ if (!minted?.client_secret) {
4760
+ throw new Error("[CheckoutManager] Stripe payment was not created");
4761
+ }
4762
+ const account = minted.stripe_account ?? await this.resolveStripeConnectAccount(spId);
4763
+ const intent = account ? { ...minted, stripe_account: account } : minted;
4764
+ const theme = readVioTheme();
4765
+ const handle = await mountStripeElement(container, intent, {
4766
+ returnUrl: resolveHttpsReturnUrl(null, {
4767
+ vio_payment: "success",
4768
+ vio_method: "stripe",
4769
+ vio_sponsor: spId,
4770
+ checkout_id: checkoutId
4771
+ }),
4772
+ theme: { accent: theme?.accent, radiusMd: theme?.radiusMd },
4773
+ onReady: callbacks?.onReady,
4774
+ onLoadError: callbacks?.onLoadError
4775
+ });
4776
+ this.stripeHandle = handle;
4777
+ return { handle, intent, checkoutId };
4778
+ }
4779
+ /** Unmount the Payment Element. Safe when nothing is mounted. */
4780
+ destroyStripe() {
4781
+ try {
4782
+ this.stripeHandle?.unmount();
4783
+ } catch {
4784
+ }
4785
+ this.stripeHandle = null;
4786
+ }
3580
4787
  /**
3581
4788
  * The Stripe publishable key for a sponsor, taken from its own commerce
3582
4789
  * channel (`GetAvailablePaymentMethods` → Stripe → config.publishableKey).
@@ -3594,6 +4801,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
3594
4801
  const methods = await this.getAvailablePaymentMethods(sponsorId);
3595
4802
  const stripe = methods?.find((m) => String(m?.name ?? "").toLowerCase() === "stripe");
3596
4803
  const key = stripe?.config?.find((c) => c?.name === "publishableKey")?.value;
4804
+ const account = stripe?.config?.find((c) => c?.name === "stripeAccount")?.value;
4805
+ this.stripeConnectAccount = typeof account === "string" && account.startsWith("acct_") ? account : null;
3597
4806
  if (typeof key === "string" && key.startsWith("pk_")) {
3598
4807
  this.applePayPublishableKey = key;
3599
4808
  return key;
@@ -4217,9 +5426,9 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4217
5426
  if (typeof sessionStorage === "undefined") return;
4218
5427
  const raw = sessionStorage.getItem(KLARNA_PENDING_KEY);
4219
5428
  if (!raw) return;
4220
- let pending;
5429
+ let pending2;
4221
5430
  try {
4222
- pending = JSON.parse(raw);
5431
+ pending2 = JSON.parse(raw);
4223
5432
  } catch {
4224
5433
  sessionStorage.removeItem(KLARNA_PENDING_KEY);
4225
5434
  return;
@@ -4227,7 +5436,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4227
5436
  const fromUrl = readKlarnaTokenFromUrl();
4228
5437
  if (fromUrl) {
4229
5438
  cleanUrlParams([fromUrl.param]);
4230
- await this.completeKlarnaOrder(fromUrl.token, pending, { authorizationToken: fromUrl.token });
5439
+ await this.completeKlarnaOrder(fromUrl.token, pending2, { authorizationToken: fromUrl.token });
4231
5440
  return;
4232
5441
  }
4233
5442
  if (typeof window !== "undefined" && typeof console !== "undefined" && window.location.search) {
@@ -4241,7 +5450,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4241
5450
  await listenForKlarnaCompletion({
4242
5451
  config: { clientId: cfg.klarnaClientId, locale: getGlobalLocale(), environment: cfg.klarnaEnvironment },
4243
5452
  onComplete: (result) => {
4244
- void this.completeKlarnaOrder(result.authorizationToken, pending, result);
5453
+ void this.completeKlarnaOrder(result.authorizationToken, pending2, result);
4245
5454
  },
4246
5455
  onError: (err) => {
4247
5456
  if (typeof console !== "undefined") {
@@ -4354,7 +5563,18 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4354
5563
  * (Stripe will route to the platform account in that case).
4355
5564
  */
4356
5565
  findStripeConnectAccount() {
4357
- return void 0;
5566
+ return this.stripeConnectAccount ?? void 0;
5567
+ }
5568
+ /** The sponsor's connected account, read fresh from its channel config. */
5569
+ async resolveStripeConnectAccount(sponsorId) {
5570
+ try {
5571
+ const methods = await this.getAvailablePaymentMethods(sponsorId);
5572
+ const stripe = methods?.find((m) => String(m?.name ?? "").toLowerCase() === "stripe");
5573
+ const account = stripe?.config?.find((c) => c?.name === "stripeAccount")?.value;
5574
+ return typeof account === "string" && account.startsWith("acct_") ? account : void 0;
5575
+ } catch {
5576
+ return void 0;
5577
+ }
4358
5578
  }
4359
5579
  emit() {
4360
5580
  this.dispatchEvent(
@@ -4368,9 +5588,6 @@ function toSmallestUnit(amount, currency) {
4368
5588
  return Math.round(amount * 100);
4369
5589
  }
4370
5590
 
4371
- // src/core/version.ts
4372
- var SDK_VERSION = "0.11.1";
4373
-
4374
5591
  // src/core/analytics/analytics-manager.ts
4375
5592
  var SESSION_TTL_MS = 30 * 60 * 1e3;
4376
5593
  var FLUSH_INTERVAL_MS = 5e3;
@@ -4532,15 +5749,16 @@ var AnalyticsManager = class extends EventTarget {
4532
5749
  if (!Configuration.isInitialized) return;
4533
5750
  if (Date.now() < this.retryNotBefore) return;
4534
5751
  const batch = this.queue.splice(0, MAX_QUEUE);
5752
+ const apiKey = Configuration.get().apiKey;
4535
5753
  const body = JSON.stringify({
4536
- apiKey: Configuration.get().apiKey,
5754
+ apiKey,
4537
5755
  sent_at: (/* @__PURE__ */ new Date()).toISOString(),
4538
5756
  events: batch
4539
5757
  });
4540
5758
  try {
4541
5759
  const res = await fetch(`${eventsBase()}/v1/events`, {
4542
5760
  method: "POST",
4543
- headers: { "Content-Type": "application/json" },
5761
+ headers: { "Content-Type": "application/json", "X-Api-Key": apiKey },
4544
5762
  body,
4545
5763
  keepalive: batch.length <= 20
4546
5764
  // keepalive caps payload at ~64KB
@@ -4568,7 +5786,7 @@ var AnalyticsManager = class extends EventTarget {
4568
5786
  const boot = vioBootstrapCache();
4569
5787
  const ctx = opts.context ?? {};
4570
5788
  const context = compact({
4571
- campaign_id: ctx.campaignId ?? boot?.campaignId,
5789
+ campaign_id: resolveCampaignId(ctx, boot),
4572
5790
  broadcast_id: ctx.broadcastId,
4573
5791
  campaign_component_id: ctx.campaignComponentId,
4574
5792
  app_placement_id: ctx.appPlacementId,
@@ -4812,8 +6030,10 @@ var AnalyticsManager = class extends EventTarget {
4812
6030
  this.track("purchase", {
4813
6031
  context: { sponsorId },
4814
6032
  commerce: {
6033
+ // value / currency / items: the cart snapshot, on every path.
4815
6034
  ...snap ?? {},
4816
- orderId: d.orderId,
6035
+ // order_id: Vio's id for the purchase (never the provider's).
6036
+ ...purchaseFromPaymentSuccess(d),
4817
6037
  paymentMethod: d.method
4818
6038
  }
4819
6039
  });
@@ -4911,6 +6131,52 @@ function vioCart() {
4911
6131
  return null;
4912
6132
  }
4913
6133
  }
6134
+ function resolveCampaignId(ctx, boot) {
6135
+ if (ctx.campaignId !== void 0) return ctx.campaignId;
6136
+ const defaultId = boot?.campaign?.id;
6137
+ const sponsorId = toNumber(ctx.sponsorId);
6138
+ if (sponsorId === void 0) return defaultId;
6139
+ try {
6140
+ const campaigns = Array.isArray(boot?.campaigns) ? boot.campaigns : [];
6141
+ const matches = campaigns.filter(
6142
+ (c) => !!c && typeof c === "object" && c.status === "active" && campaignSponsorIds(c).includes(sponsorId)
6143
+ );
6144
+ const chosen = matches.find((c) => c.id === defaultId) ?? matches[0];
6145
+ const chosenId = toNumber(chosen?.id);
6146
+ if (chosenId !== void 0) return chosenId;
6147
+ } catch {
6148
+ }
6149
+ return defaultId;
6150
+ }
6151
+ function campaignSponsorIds(campaign) {
6152
+ const ids = [];
6153
+ const primary = toNumber(campaign.primarySponsor?.id);
6154
+ if (primary !== void 0) ids.push(primary);
6155
+ const secondaries = Array.isArray(campaign.secondarySponsors) ? campaign.secondarySponsors : [];
6156
+ for (const s of secondaries) {
6157
+ const id = toNumber(s?.id);
6158
+ if (id !== void 0) ids.push(id);
6159
+ }
6160
+ return ids;
6161
+ }
6162
+ function purchaseFromPaymentSuccess(d) {
6163
+ const r = d.result ?? {};
6164
+ const orderId = firstId(d.orderId, r.confirmation?.orderId, d.checkoutId);
6165
+ return orderId !== void 0 ? { orderId } : {};
6166
+ }
6167
+ function firstId(...candidates) {
6168
+ for (const c of candidates) {
6169
+ if (c === void 0 || c === null) continue;
6170
+ const s = String(c);
6171
+ if (s !== "") return s;
6172
+ }
6173
+ return void 0;
6174
+ }
6175
+ function toNumber(value) {
6176
+ if (value === void 0 || value === null || value === "") return void 0;
6177
+ const n = Number(value);
6178
+ return Number.isFinite(n) ? n : void 0;
6179
+ }
4914
6180
  function pushGa4(event) {
4915
6181
  const commerceNames = /* @__PURE__ */ new Set([
4916
6182
  "view_item_list",
@@ -4956,6 +6222,10 @@ var VioFacade = class {
4956
6222
  /** Last bootstrap failure — short backoff so cart mutations don't hammer a
4957
6223
  * dead endpoint on every call. */
4958
6224
  this.bootstrapFailedAt = 0;
6225
+ /** Bumped by every init(). A bootstrap started under an older configuration
6226
+ * (another key or environment) is still in flight after a re-init; its answer
6227
+ * belongs to that configuration and must not land in this one. */
6228
+ this.generation = 0;
4959
6229
  // Client-side managers — created lazily on first access. They don't
4960
6230
  // need Vio.init() to work (state is purely local).
4961
6231
  this.cartManager = null;
@@ -4969,6 +6239,7 @@ var VioFacade = class {
4969
6239
  this.bootstrapPromise = null;
4970
6240
  this.cachedBootstrap = null;
4971
6241
  this.bootstrapFailedAt = 0;
6242
+ this.generation++;
4972
6243
  this.checkoutManager?.clearPaymentMethodsCache();
4973
6244
  }
4974
6245
  get isInitialized() {
@@ -4997,7 +6268,7 @@ var VioFacade = class {
4997
6268
  return this.analyticsManager;
4998
6269
  }
4999
6270
  // MARK: - Backend-bound
5000
- /** Lazy VioApi for REST endpoints on `api-dev.vio.live`. */
6271
+ /** Lazy VioApi for REST endpoints on `api-staging.vio.live`. */
5001
6272
  get api() {
5002
6273
  if (!this.apiInstance) {
5003
6274
  const cfg = Configuration.get();
@@ -5008,6 +6279,11 @@ var VioFacade = class {
5008
6279
  /**
5009
6280
  * Bootstrap — fetches client config and caches it. Dedupes in-flight
5010
6281
  * concurrent requests.
6282
+ *
6283
+ * A request that was started before the last init() (Vev starts with the key
6284
+ * the browser remembers, then the page's own key arrives) answers for a key
6285
+ * that is gone: it neither fills the cache nor arms the failure backoff, and
6286
+ * whoever awaited it gets the current configuration's bootstrap instead.
5011
6287
  */
5012
6288
  async bootstrap() {
5013
6289
  if (this.cachedBootstrap) return this.cachedBootstrap;
@@ -5015,17 +6291,24 @@ var VioFacade = class {
5015
6291
  if (this.bootstrapFailedAt && Date.now() - this.bootstrapFailedAt < 5e3) {
5016
6292
  throw new Error("[Vio] bootstrap recently failed \u2014 backing off");
5017
6293
  }
5018
- this.bootstrapPromise = this.api.bootstrap().then((res) => {
5019
- this.cachedBootstrap = res;
5020
- this.bootstrapFailedAt = 0;
5021
- return res;
5022
- }).catch((err) => {
5023
- this.bootstrapFailedAt = Date.now();
5024
- throw err;
5025
- }).finally(() => {
5026
- this.bootstrapPromise = null;
6294
+ const generation = this.generation;
6295
+ const promise = this.api.bootstrap().then(
6296
+ (res) => {
6297
+ if (generation !== this.generation) return this.bootstrap();
6298
+ this.cachedBootstrap = res;
6299
+ this.bootstrapFailedAt = 0;
6300
+ return res;
6301
+ },
6302
+ (err) => {
6303
+ if (generation !== this.generation) return this.bootstrap();
6304
+ this.bootstrapFailedAt = Date.now();
6305
+ throw err;
6306
+ }
6307
+ ).finally(() => {
6308
+ if (this.bootstrapPromise === promise) this.bootstrapPromise = null;
5027
6309
  });
5028
- return this.bootstrapPromise;
6310
+ this.bootstrapPromise = promise;
6311
+ return promise;
5029
6312
  }
5030
6313
  get bootstrapCache() {
5031
6314
  return this.cachedBootstrap;
@@ -5065,6 +6348,55 @@ var VioFacade = class {
5065
6348
  var _vioGlobal = globalThis;
5066
6349
  var Vio = _vioGlobal.__VIO_FACADE__ ?? (_vioGlobal.__VIO_FACADE__ = new VioFacade());
5067
6350
 
5068
- export { ADD_ITEM_MUTATION, AnalyticsManager, 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, SDK_VERSION, 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 };
5069
- //# sourceMappingURL=chunk-TJ43TE7H.js.map
5070
- //# sourceMappingURL=chunk-TJ43TE7H.js.map
6351
+ // src/core/checkout/payments/vipps-button.ts
6352
+ var VIPPS_BUTTON_SCRIPT = "https://cdn.vippsmobilepay.com/js/button/button.js";
6353
+ var VIPPS_BUTTON_TAG = "vipps-mobilepay-button";
6354
+ var pending = null;
6355
+ function ensureVippsButton(timeoutMs = 0) {
6356
+ if (typeof document === "undefined" || typeof customElements === "undefined") {
6357
+ return Promise.resolve(false);
6358
+ }
6359
+ if (customElements.get(VIPPS_BUTTON_TAG)) return Promise.resolve(true);
6360
+ if (!pending) {
6361
+ pending = new Promise((resolve) => {
6362
+ let settled = false;
6363
+ const done = (ok) => {
6364
+ if (settled) return;
6365
+ settled = true;
6366
+ if (timer) clearTimeout(timer);
6367
+ resolve(ok);
6368
+ };
6369
+ const timer = timeoutMs > 0 ? setTimeout(() => done(false), timeoutMs) : void 0;
6370
+ customElements.whenDefined(VIPPS_BUTTON_TAG).then(
6371
+ () => done(true),
6372
+ () => done(false)
6373
+ );
6374
+ if (!document.querySelector(`script[src="${VIPPS_BUTTON_SCRIPT}"]`)) {
6375
+ const script = document.createElement("script");
6376
+ script.async = true;
6377
+ script.src = VIPPS_BUTTON_SCRIPT;
6378
+ script.onerror = () => done(false);
6379
+ document.head.appendChild(script);
6380
+ }
6381
+ });
6382
+ void pending.then((ok) => {
6383
+ if (!ok) pending = null;
6384
+ });
6385
+ }
6386
+ return pending;
6387
+ }
6388
+ function vippsButtonAttributes(verb = "buy", opts = {}) {
6389
+ return {
6390
+ brand: "vipps",
6391
+ language: opts.language ?? "no",
6392
+ verb,
6393
+ variant: "primary",
6394
+ rounded: "true",
6395
+ stretched: opts.stretched === false ? "false" : "true",
6396
+ ...opts.compact ? { compact: "true" } : {}
6397
+ };
6398
+ }
6399
+
6400
+ export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, 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, KUSTOM_RETURN_QUERY_KEYS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, addItem, adyenReturnOutcome, adyenReturnParams, checkApplePayAvailability, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, ensureVippsButton, executeCartGraphQL, formatPrice, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, kustomReturnOutcome, nexiReturnPaymentId, primaryImageUrl, purchaseFromPaymentSuccess, readNexiPending, resolveCampaignId, showApplePaySheet, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes };
6401
+ //# sourceMappingURL=chunk-WTVGVDZX.js.map
6402
+ //# sourceMappingURL=chunk-WTVGVDZX.js.map