@vio-live/web-sdk 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -14,9 +14,79 @@ How to keep this file: see [CONTRIBUTING → Changelog](CONTRIBUTING.md#changelo
14
14
  The details of each feature live in the [README](README.md); this file says *what*
15
15
  and *when*, and links there.
16
16
 
17
- ## Unreleased
17
+ ## 0.29.0 — 2026-10-24
18
18
 
19
- Nothing yet.
19
+ ### Added
20
+ - **`vio:cart-updated`** on `document` — `{ sponsorId, cartId, items, itemCount, subtotal, currency, source }`,
21
+ `source: 'server'` when the cart was just read from the server (right before a checkout is
22
+ created or a payment link minted, and when the drawer or the checkout opens), `'storage'` when
23
+ another tab of the site changed it. For hosts that draw their own cart or badge.
24
+ `Vio.cart.refreshFromServer(sponsorId)` asks on demand. See
25
+ [README → What the shopper pays is what the page shows](README.md#what-the-shopper-pays-is-what-the-page-shows--the-cart-the-server-the-other-tabs).
26
+ - **`add_payment_info`** (analytics, GA4 name) the moment a backend checkout exists for a payment
27
+ — Vipps from the detail, the cart or the checkout, a Stripe payment link or Element, Klarna,
28
+ Apple Pay, Nexi, Kustom, Qliro, Walley, Adyen — with `commerce.checkout_id`, `payment_method`,
29
+ the cart's lines and the same context as `begin_checkout` (per campaign like it). Once per
30
+ checkout per page load, none when the checkout could not be created, and by beacon BEFORE the
31
+ page leaves for Vipps or a Stripe link. It is how a paid order whose `purchase` never came back
32
+ keeps its session, page and slot (ADR-0039; order 4128, 2026-10-23). The checkout manager
33
+ announces it on `window` as **`vio:checkout-created`** `{ sponsorId, checkoutId, paymentMethod,
34
+ campaignId?, redirect }`. See [README → `add_payment_info`](README.md#add_payment_info--the-checkout-linked-to-this-session-0290).
35
+
36
+ ### Changed
37
+ - **«Kjøp nå med Vipps» on the product detail goes straight to Vipps only when the cart is
38
+ empty.** With lines already in the cart, the product is added and the checkout opens first
39
+ (`vio:checkout-open`, Vipps preselected, no `vio:payment-redirect-*` events), every line and
40
+ the total on screen. Before, the tap paid the WHOLE cart with only the Vipps page saying so
41
+ (Mote & Livsstil, 2026-10-23: a jumper left in the cart 13 minutes earlier was paid along
42
+ with the top bought «nå»). The cart's own Vipps button is unchanged: it pays the lines it shows.
43
+ - **Apple Pay on the product detail shows what it charges.** The sheet's amount is fixed before
44
+ the tap and was the product's price, while the charge was the sponsor's whole cart. The sheet
45
+ now opens only on an empty cart, prepared for the product × the chosen quantity (it is prepared
46
+ again when the quantity or variant changes); with lines already in the cart — or while the
47
+ sheet for a new quantity is still being prepared — the product is added and the checkout opens
48
+ (`vio:checkout-open` with `paymentMethod: 'apple-pay'`), where Apple Pay charges the total shown.
49
+
50
+ ### Fixed
51
+ - **The lines the shopper pays for are the lines the page shows.** The checkout, and the
52
+ Vipps / Stripe page, are built on the backend cart — every line in it — while the drawer
53
+ and the checkout summary drew this tab's memory. Now the SDK reads the server's cart right
54
+ before a checkout is created or a payment link minted, and when `<vio-cart>` / `<vio-checkout>`
55
+ open, and draws that; an open checkout recomputes its lines and total when the cart changes
56
+ under it.
57
+ - **Other tabs of the site are heard.** The carts are shared through `localStorage` but each
58
+ tab read them once, at load: a payment in one tab left the others showing — and adding to —
59
+ a cart that was already paid; a line added in one tab was missing from the others until a
60
+ reload. Now a tab adopts what another tab wrote (`storage` event), except for a sponsor it is
61
+ mid-add for.
62
+ - **Vipps Express: the checkout no longer adds our shipping to the total.** The delivery is
63
+ chosen and paid inside Vipps, but with Vipps selected the checkout still added the seller's
64
+ first rate (Careless QA, 2026-10-24: «Frakt – Default Shipping» 10 kr, 1 308 kr on screen and
65
+ on the button, 1 298 kr in Vipps). With Vipps as the method and Express on for the seller,
66
+ the summary says «Frakt: Velges i Vipps» and the total and «Betal … med Vipps» are the items.
67
+ Vipps without Express, and every other method, still add the rate as before.
68
+ - **The `purchase` of a paid return is sent, not left in the queue** (2026-10-24). Order 4128
69
+ (Careless, prod, 2026-10-23 22:40, after two abandoned trips to Vipps): the shopper
70
+ paid, came back, saw the confirmation, and the brand's panel never counted the sale — Commerce
71
+ had its `order_paid`, the collector no `purchase`. The return was read as paid; the one way the
72
+ code loses that purchase is the queue: it was built, with the order number, into a queue sent
73
+ every 5 s, and leaving the page right away (back to the Vipps app, the home screen) beaconed
74
+ only a purchase still waiting for its number — a phone that discards a hidden tab never runs
75
+ the next tick. Now the purchase is sent as soon as it is built, and when the page hides
76
+ everything still queued leaves by beacon.
77
+
78
+ ### ⚠️ Upgrade notes
79
+ - **The collector must accept `add_payment_info`** before it is counted: one that does not know the
80
+ name rejects that event alone (`202` with `rejected`), the rest of the batch is kept. Nothing to
81
+ do in the host.
82
+ - A host that handles `vio:checkout-open` only from the cart: the product detail now emits it
83
+ too when «Kjøp nå med Vipps» or Apple Pay is tapped on a non-empty cart (it already did when
84
+ Express was off for the seller), now also with `paymentMethod: 'apple-pay'`. Check that your
85
+ handler opens `<vio-checkout>` for the detail's event.
86
+ - A host with its own cart drawer or badge: listen to `vio:cart-updated` (or `Vio.cart`'s
87
+ `change`) — the lines can now change without a tap in that tab.
88
+ - One more `GetCart` per checkout creation / payment link and per opening of the drawer or the
89
+ checkout.
20
90
 
21
91
  ## 0.28.0 — 2026-10-22
22
92
 
package/README.md CHANGED
@@ -487,11 +487,38 @@ could never be netted.
487
487
  arrives after the event left with the checkout id adds no second event. `ts`
488
488
  and `event_id` are fixed at the payment, whenever the event leaves; a purchase
489
489
  split per campaign waits once and every slice carries the same number.
490
+ - **Sent at once.** A `purchase` does not wait for the queue's 5 s tick: it is
491
+ sent as soon as it is built, and when the page hides (`visibilitychange`, the
492
+ last thing a phone is sure to say before it discards a tab) whatever is still
493
+ queued leaves by beacon (0.29.0; order 4128 had been recorded and never sent).
490
494
  - **A host with its own sink** (Vev) reads the same ids from the detail with
491
495
  `purchaseIdsOf(detail)` → `{ orderId, checkoutId, orderIdKind }` (or
492
496
  `purchaseFromPaymentSuccess(detail)`, exported from `/core`), and waits for the
493
497
  number the same way through `Vio.checkout.getOrderReference(checkoutId)`.
494
498
 
499
+ ### `add_payment_info` — the checkout, linked to this session (0.29.0)
500
+
501
+ The moment a backend checkout exists for a payment — Vipps (from the product
502
+ detail, the cart or the checkout), a Stripe payment link or Element, Klarna, Apple
503
+ Pay, Nexi, Kustom, Qliro, Walley, Adyen — the SDK emits **`add_payment_info`**
504
+ (the GA4 name) with **`commerce.checkout_id`**, **`payment_method`**, the cart's
505
+ lines (`items`, `value`, `currency`) and the same context as `begin_checkout`
506
+ (sponsor, campaign, slot, content), split per campaign like it. Once per checkout
507
+ per page load; nothing when the checkout could not be created. When the page is
508
+ about to leave for the provider (Vipps, a Stripe payment link) it goes out by
509
+ beacon first.
510
+
511
+ Why (ADR-0039): Commerce's `order_paid` is what says a sale happened, and the
512
+ browser's `purchase` can be missing (a closed tab, a phone that discards the page
513
+ on the way back). The collector attributes an `order_paid` without a `purchase` to
514
+ the session, page and slot of the `add_payment_info` with the same `checkout_id`.
515
+
516
+ The checkout manager announces each checkout on `window` as
517
+ **`vio:checkout-created`** — `{ sponsorId, checkoutId, paymentMethod, campaignId?,
518
+ redirect }` — which is what analytics listens to; a host with its own sink can
519
+ listen to it too. With `dataLayer: true` it is pushed as GA4's `add_payment_info`
520
+ with `payment_type`.
521
+
495
522
  ## Sponsor identity — name and logo
496
523
 
497
524
  The elements say **who sells**: the sponsor's (brand's) display name and logo.
@@ -674,6 +701,48 @@ tap, so a second payment cannot start. Events on `document`:
674
701
  `source` is `'detail'`, `'cart'` or `'checkout'`. The overlay is `.vio-redirect-overlay`
675
702
  in the light DOM.
676
703
 
704
+ **The detail's Vipps button pays the whole cart.** «Kjøp nå med Vipps» on the product
705
+ detail adds the product to the sponsor's cart and the checkout is built on that cart —
706
+ every line in it. It goes straight to Vipps only when the cart was **empty**; with lines
707
+ already in it, the product is added and the checkout opens first (`vio:checkout-open`
708
+ with `paymentMethod: 'vipps'`, no hand-over events), every line and the total on screen,
709
+ so what the Vipps page lists is what the shopper saw. The cart's own Vipps button always
710
+ pays the whole cart — the lines are right there. **Apple Pay on the detail follows the same
711
+ rule**: its sheet's amount is fixed before the tap, so it opens only on an empty cart, for the
712
+ product × the chosen quantity; otherwise the checkout opens (`paymentMethod: 'apple-pay'`).
713
+
714
+ ### What the shopper pays is what the page shows — the cart, the server, the other tabs
715
+
716
+ The cart lives in this tab's memory and in `localStorage` (`vio.cart.v1`, one entry per
717
+ sponsor with the backend cart id), and the checkout — and the Vipps / Stripe page — is
718
+ built on the **backend** cart, every line of it. Three things keep the two the same:
719
+
720
+ - **Before a checkout is created or a payment link minted**, and **when `<vio-cart>` or
721
+ `<vio-checkout>` opens**, the SDK asks the server for the cart (`GetCart`) and draws that
722
+ answer: a line another tab added, a cart another tab emptied. `Vio.cart.refreshFromServer(sponsorId)`
723
+ does the same on demand and resolves with the sponsor's cart. A `GetCart` that fails leaves
724
+ the cart as it was (and warns) — the checkout then goes on with the lines the last mutation
725
+ answered, as before.
726
+ - **Other tabs of the site are heard.** A payment in one tab (its cart cleared) empties the
727
+ drawer of the others and the next add there opens a new backend cart; a line added in one
728
+ tab is in the others without a reload. Through the browser's `storage` event — a tab that
729
+ is mid-add for a sponsor keeps its own lines until its answer lands.
730
+ - **An open checkout follows its cart**: its lines and its total are recomputed when the
731
+ cart changes under it (the widgets that captured an amount at mount ask to be refreshed,
732
+ as they already did for a changed cart).
733
+
734
+ `vio:cart-updated` on `document` says when either of the first two happened:
735
+
736
+ ```ts
737
+ document.addEventListener('vio:cart-updated', (e) => {
738
+ const { sponsorId, cartId, items, itemCount, subtotal, currency, source } = e.detail
739
+ // source: 'server' (GetCart, right before a checkout / on opening) | 'storage' (another tab)
740
+ })
741
+ ```
742
+
743
+ `items` are copies of the lines (`CartLineItem[]`). A host that draws its own cart or badge
744
+ redraws it from this event; `<vio-cart>` and the badge wired to `Vio.cart`'s `change` need nothing.
745
+
677
746
  ## Headless / SSR
678
747
 
679
748
  The root and `/core` entries export the framework-agnostic core with **no DOM side-effects** — SSR-safe and **tree-shakeable** (importing one helper pulls only that helper, ~0.5 kB):
@@ -2214,7 +2214,36 @@ var CartManager = class extends EventTarget {
2214
2214
  /** Quantity intents for lines whose backend id hasn't arrived yet — replayed
2215
2215
  * when the AddItem response reconciles (keyed by product::variant). */
2216
2216
  this.pendingQtyIntents = /* @__PURE__ */ new Map();
2217
+ /**
2218
+ * Another tab of the site wrote the carts: adopt what it wrote, sponsor by
2219
+ * sponsor — its lines and its backend cart id, or nothing when it cleared
2220
+ * the cart (a payment) — except for a sponsor this tab is mid-write for:
2221
+ * its own answer lands in a moment and is persisted then, over whatever
2222
+ * the other tab had. `key` is null when the other tab cleared the whole
2223
+ * storage. Says `change` (the drawer) and `vio:cart-updated` per sponsor
2224
+ * that changed (`source: 'storage'`); nothing when nothing did.
2225
+ */
2226
+ this.onStorage = (e) => {
2227
+ if (e.key !== null && e.key !== STORAGE_KEY) return;
2228
+ const stored = this.loadFromStorage();
2229
+ const changed = [];
2230
+ for (const sponsorId of /* @__PURE__ */ new Set([...this.cartsBySponsor.keys(), ...stored.keys()])) {
2231
+ if (this.hasMutationsInFlight(sponsorId) || this.cartIdPromises.has(sponsorId)) continue;
2232
+ const mine = this.cartsBySponsor.get(sponsorId);
2233
+ const theirs = stored.get(sponsorId);
2234
+ if (JSON.stringify(mine ?? null) === JSON.stringify(theirs ?? null)) continue;
2235
+ if (theirs) this.cartsBySponsor.set(sponsorId, theirs);
2236
+ else this.cartsBySponsor.delete(sponsorId);
2237
+ changed.push(sponsorId);
2238
+ }
2239
+ if (changed.length === 0) return;
2240
+ this.emit();
2241
+ for (const sponsorId of changed) this.emitCartUpdated(sponsorId, "storage");
2242
+ };
2217
2243
  this.cartsBySponsor = this.loadFromStorage();
2244
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
2245
+ window.addEventListener("storage", this.onStorage);
2246
+ }
2218
2247
  }
2219
2248
  nextSeq(sponsorId) {
2220
2249
  const n = (this.mutationSeq.get(sponsorId) ?? 0) + 1;
@@ -2374,6 +2403,9 @@ var CartManager = class extends EventTarget {
2374
2403
  * Overwrite local cart state with the backend cart response (source of
2375
2404
  * truth). `opts.settling` is the mutation whose answer this is: it does not
2376
2405
  * count as "in flight" for the origins (see `hasMutationsInFlight`).
2406
+ * `opts.authoritative` is a GetCart taken with nothing of this tab's in
2407
+ * flight (`refreshFromServer`): an empty answer then means the cart IS
2408
+ * empty, and the local lines go — not an add still on its way.
2377
2409
  */
2378
2410
  updateFromBackendCart(sponsorId, backendCart, opts = {}) {
2379
2411
  const cart = this.cartsBySponsor.get(sponsorId);
@@ -2389,7 +2421,7 @@ var CartManager = class extends EventTarget {
2389
2421
  const removedBeforeSync = [];
2390
2422
  if (Array.isArray(backendCart.line_items)) {
2391
2423
  const existingItems = cart.items || [];
2392
- if (backendCart.line_items.length === 0 && existingItems.length > 0) {
2424
+ if (backendCart.line_items.length === 0 && existingItems.length > 0 && !opts.authoritative) {
2393
2425
  this.persist();
2394
2426
  this.emit();
2395
2427
  return;
@@ -2743,6 +2775,60 @@ var CartManager = class extends EventTarget {
2743
2775
  if (resCart) this.updateFromBackendCart(sponsorId, resCart);
2744
2776
  return resCart;
2745
2777
  }
2778
+ /**
2779
+ * The cart as the server has it, for the moment a checkout is about to be
2780
+ * built on it: what the shopper is about to pay must be what the page shows.
2781
+ * Waits for this tab's own mutations (their answers are the server's too),
2782
+ * then applies GetCart as it is — a line another tab added, one it removed,
2783
+ * an empty cart — and tells the page: `change` for the drawer, and
2784
+ * `vio:cart-updated` on `document` (`source: 'server'`) for a host that
2785
+ * draws its own. A mutation that starts while the server is answering makes
2786
+ * that answer old: it is asked once more. Nothing to ask without a backend
2787
+ * cart. A GetCart that fails leaves the local state as it was and does not
2788
+ * throw — the checkout then goes on with the lines the last mutation
2789
+ * answered, as it always did. Resolves with the sponsor's cart.
2790
+ */
2791
+ async refreshFromServer(sponsorId) {
2792
+ for (let attempt = 0; attempt < 2; attempt++) {
2793
+ await this.waitForCartMutations(sponsorId);
2794
+ const cart = this.cartsBySponsor.get(sponsorId);
2795
+ if (!cart?.cartId) return cart;
2796
+ let resCart;
2797
+ try {
2798
+ const gqlOpts = await getCartGraphQLOptions(sponsorId);
2799
+ resCart = await getCart(cart.cartId, gqlOpts);
2800
+ } catch (err) {
2801
+ if (typeof console !== "undefined") {
2802
+ console.warn("[CartManager] GetCart before the checkout failed \u2014 the local cart stands", err);
2803
+ }
2804
+ return cart;
2805
+ }
2806
+ if (this.hasMutationsInFlight(sponsorId)) continue;
2807
+ const backendCart = resCart;
2808
+ if (backendCart && Array.isArray(backendCart.line_items)) {
2809
+ this.updateFromBackendCart(sponsorId, backendCart, { authoritative: true });
2810
+ this.emitCartUpdated(sponsorId, "server");
2811
+ }
2812
+ return this.cartsBySponsor.get(sponsorId);
2813
+ }
2814
+ return this.cartsBySponsor.get(sponsorId);
2815
+ }
2816
+ /** `vio:cart-updated` on `document` — see `CartUpdatedDetail`. */
2817
+ emitCartUpdated(sponsorId, source) {
2818
+ if (typeof document === "undefined") return;
2819
+ const cart = this.cartsBySponsor.get(sponsorId);
2820
+ const items = (cart?.items ?? []).map((item) => ({ ...item }));
2821
+ const detail = {
2822
+ sponsorId,
2823
+ cartId: cart?.cartId,
2824
+ items,
2825
+ itemCount: items.reduce((count, item) => count + item.quantity, 0),
2826
+ subtotal: items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0),
2827
+ currency: cart?.currency || getGlobalCurrency(),
2828
+ source
2829
+ };
2830
+ document.dispatchEvent(new CustomEvent("vio:cart-updated", { bubbles: true, detail }));
2831
+ }
2746
2832
  /** Line items grouped by supplier (shipping is per-supplier server-side). */
2747
2833
  async fetchLineItemsBySupplier(sponsorId) {
2748
2834
  const cart = this.cartsBySponsor.get(sponsorId);
@@ -3267,7 +3353,7 @@ async function prepareApplePay(config) {
3267
3353
  }
3268
3354
 
3269
3355
  // src/core/version.ts
3270
- var SDK_VERSION = "0.28.0";
3356
+ var SDK_VERSION = "0.29.0";
3271
3357
 
3272
3358
  // src/core/checkout/payments/stripe-embedded.ts
3273
3359
  var CREATE_PAYMENT_INTENT_STRIPE_MUTATION = `
@@ -4729,6 +4815,11 @@ function adyenReturnUrl(params) {
4729
4815
  bare.search = "";
4730
4816
  return build(bare);
4731
4817
  }
4818
+ function paymentMethodName(method) {
4819
+ if (typeof method !== "string") return void 0;
4820
+ const name = method.trim().toLowerCase().replace(/\s+/g, "-");
4821
+ return name || void 0;
4822
+ }
4732
4823
  function resolveHttpsReturnUrl(overrideUrl, params = null) {
4733
4824
  let url = overrideUrl ?? void 0;
4734
4825
  if (!url && typeof window !== "undefined" && window.location) {
@@ -4771,6 +4862,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4771
4862
  this.currentStateValue = null;
4772
4863
  /** The last checkout + identity written to the memory — the state is set on every keystroke of the form. */
4773
4864
  this.lastRememberedSponsor = "";
4865
+ /** Backend checkouts this page load announced with `vio:checkout-created`: one event each. */
4866
+ this.announcedCheckouts = /* @__PURE__ */ new Set();
4774
4867
  /** Counts `open()` calls — see CheckoutState.session. */
4775
4868
  this.sessionSeq = 0;
4776
4869
  this.applePayAvailableCache = null;
@@ -4809,10 +4902,15 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4809
4902
  this.paymentMethodsCache = /* @__PURE__ */ new Map();
4810
4903
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4811
4904
  this.paymentMethodsPromises = /* @__PURE__ */ new Map();
4905
+ /** What the open checkout's cart looked like when it was last emitted. */
4906
+ this.lastCartSignature = null;
4812
4907
  /** In-flight `getOrderReference` per checkout id (see there). */
4813
4908
  this.referenceInFlight = /* @__PURE__ */ new Map();
4814
4909
  this.kustomController = null;
4815
4910
  preloadStripeJs();
4911
+ if (typeof cartManager.addEventListener === "function") {
4912
+ cartManager.addEventListener("change", () => this.onCartChange());
4913
+ }
4816
4914
  }
4817
4915
  /**
4818
4916
  * Every write of the state goes through here: a state that names its
@@ -4826,6 +4924,21 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4826
4924
  this.currentStateValue = next;
4827
4925
  if (next?.checkoutId && next.sponsorId) this.rememberSponsorOfCheckout(next.checkoutId, next.sponsorId);
4828
4926
  }
4927
+ /** The open checkout's lines as a signature, to tell a real change from an echo. */
4928
+ cartSignature(sponsorId) {
4929
+ return (this.cartManager.getCart(sponsorId)?.items ?? []).map((i) => `${i.productId}:${i.variantId ?? ""}:${i.quantity}:${i.unitPrice}`).join("|");
4930
+ }
4931
+ /** The open checkout follows its cart: subtotal recomputed and `change` said when the lines differ. */
4932
+ onCartChange() {
4933
+ const state = this.state;
4934
+ if (!state) return;
4935
+ const subtotal = this.cartManager.subtotalForSponsor(state.sponsorId);
4936
+ const signature = this.cartSignature(state.sponsorId);
4937
+ if (subtotal === state.subtotal && signature === this.lastCartSignature) return;
4938
+ this.lastCartSignature = signature;
4939
+ this.state = { ...state, subtotal };
4940
+ this.emit();
4941
+ }
4829
4942
  open(sponsorId) {
4830
4943
  const cart = this.cartManager.getCart(sponsorId);
4831
4944
  if (!cart || cart.items.length === 0) {
@@ -4839,6 +4952,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4839
4952
  // the host may have switched currency since the cart was created.
4840
4953
  currency: getGlobalCurrency()
4841
4954
  };
4955
+ this.lastCartSignature = this.cartSignature(sponsorId);
4842
4956
  this.emit();
4843
4957
  return this.state;
4844
4958
  }
@@ -4863,9 +4977,15 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4863
4977
  return void 0;
4864
4978
  }
4865
4979
  }
4866
- /** Create the backend checkout for the sponsor's cart (auto-accepts terms); born attributed when the facade knows the attribution. */
4867
- async createCheckout(sponsorId) {
4980
+ /**
4981
+ * Create the backend checkout for the sponsor's cart (auto-accepts terms);
4982
+ * born attributed when the facade knows the attribution. `paymentMethod` is
4983
+ * the method it is created to pay with, when the caller knows it (else the
4984
+ * state's): it goes on `vio:checkout-created`.
4985
+ */
4986
+ async createCheckout(sponsorId, paymentMethod) {
4868
4987
  await this.cartManager.waitForCartMutations(sponsorId);
4988
+ await this.cartManager.refreshFromServer(sponsorId);
4869
4989
  const cart = this.cartManager.getCart(sponsorId);
4870
4990
  let cartId = cart?.cartId;
4871
4991
  if (!cartId) {
@@ -4885,6 +5005,9 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4885
5005
  checkout
4886
5006
  };
4887
5007
  this.emit();
5008
+ this.announceCheckout(checkout.id, sponsorId, paymentMethod ?? this.state?.paymentMethod, {
5009
+ campaignId: attribution?.campaign_id
5010
+ });
4888
5011
  try {
4889
5012
  const updated = await updateCheckout(
4890
5013
  {
@@ -4907,6 +5030,35 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4907
5030
  }
4908
5031
  return checkout;
4909
5032
  }
5033
+ /**
5034
+ * `vio:checkout-created` on `window` — `{ sponsorId, checkoutId,
5035
+ * paymentMethod, campaignId?, redirect }` — the moment a backend checkout
5036
+ * exists for a payment, before anything is asked of the provider. Analytics
5037
+ * answers it with `add_payment_info` (ADR-0039: the one link from the
5038
+ * order Commerce confirms back to this browser's session); `redirect: true`
5039
+ * when the page is about to leave for the provider. Once per checkout per
5040
+ * page load; never throws.
5041
+ */
5042
+ announceCheckout(checkoutId, sponsorId, paymentMethod, opts = {}) {
5043
+ if (!checkoutId || this.announcedCheckouts.has(checkoutId)) return;
5044
+ this.announcedCheckouts.add(checkoutId);
5045
+ if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") return;
5046
+ const method = paymentMethodName(paymentMethod);
5047
+ try {
5048
+ window.dispatchEvent(
5049
+ new CustomEvent("vio:checkout-created", {
5050
+ detail: {
5051
+ sponsorId,
5052
+ checkoutId,
5053
+ ...method ? { paymentMethod: method } : {},
5054
+ ...typeof opts.campaignId === "number" ? { campaignId: opts.campaignId } : {},
5055
+ redirect: opts.redirect === true
5056
+ }
5057
+ })
5058
+ );
5059
+ } catch {
5060
+ }
5061
+ }
4910
5062
  /** Update the backend checkout (terms are re-asserted unless overridden; the attribution rides along unless the caller gives its own). */
4911
5063
  async updateCheckout(variables, sponsorId) {
4912
5064
  const spId = sponsorId ?? this.state?.sponsorId;
@@ -5136,10 +5288,11 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5136
5288
  * backend cart + checkout exist, push email/address onto the checkout, and
5137
5289
  * return everything the payment mutation needs.
5138
5290
  */
5139
- async prepareRedirectPayment(sponsorId, formData, extraUpdateVars = {}) {
5291
+ async prepareRedirectPayment(sponsorId, formData, extraUpdateVars = {}, leaving = false) {
5140
5292
  const spId = sponsorId ?? this.state?.sponsorId;
5141
5293
  if (!spId) throw new Error("[CheckoutManager] no sponsor for payment");
5142
5294
  await this.cartManager.waitForCartMutations(spId);
5295
+ await this.cartManager.refreshFromServer(spId);
5143
5296
  const cart = this.cartManager.getCart(spId);
5144
5297
  let cartId = cart?.cartId;
5145
5298
  if (!cartId) {
@@ -5154,6 +5307,10 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5154
5307
  if (!checkoutId) {
5155
5308
  const checkoutRes = attribution ? await createCheckout(cartId, opts, { attribution }) : await createCheckout(cartId, opts);
5156
5309
  checkoutId = checkoutRes?.id;
5310
+ this.announceCheckout(checkoutId, spId, String(extraUpdateVars.paymentMethod ?? "") || this.state?.paymentMethod, {
5311
+ redirect: leaving,
5312
+ campaignId: attribution?.campaign_id
5313
+ });
5157
5314
  }
5158
5315
  if (!checkoutId) {
5159
5316
  throw new Error("[CheckoutManager] no backend checkout \u2014 cannot start the payment");
@@ -5222,7 +5379,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5222
5379
  const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
5223
5380
  sponsorId,
5224
5381
  formData,
5225
- { paymentMethod: "Stripe" }
5382
+ { paymentMethod: "Stripe" },
5383
+ true
5226
5384
  );
5227
5385
  const successUrl = resolveHttpsReturnUrl(null, {
5228
5386
  vio_payment: "success",
@@ -5261,7 +5419,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5261
5419
  const { spId, checkoutId, emailVal, opts } = await this.prepareRedirectPayment(
5262
5420
  sponsorId,
5263
5421
  formData,
5264
- { paymentMethod: "Vipps" }
5422
+ { paymentMethod: "Vipps" },
5423
+ true
5265
5424
  );
5266
5425
  const returnUrl = resolveHttpsReturnUrl(null, {
5267
5426
  vio_payment: "success",
@@ -5318,7 +5477,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5318
5477
  const opts = await getCartGraphQLOptions(spId);
5319
5478
  let checkoutId = this.state?.checkoutId;
5320
5479
  if (!checkoutId) {
5321
- const checkoutRes = await this.createCheckout(spId);
5480
+ const checkoutRes = await this.createCheckout(spId, "kustom");
5322
5481
  checkoutId = checkoutRes?.id;
5323
5482
  if (checkoutId && this.state) {
5324
5483
  this.state = { ...this.state, checkoutId };
@@ -5394,7 +5553,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5394
5553
  const opts = await getCartGraphQLOptions(spId);
5395
5554
  let checkoutId = this.state?.checkoutId;
5396
5555
  if (!checkoutId) {
5397
- const checkoutRes = await this.createCheckout(spId);
5556
+ const checkoutRes = await this.createCheckout(spId, "qliro");
5398
5557
  checkoutId = checkoutRes?.id;
5399
5558
  if (checkoutId && this.state) {
5400
5559
  this.state = { ...this.state, checkoutId };
@@ -5482,7 +5641,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5482
5641
  const opts = await getCartGraphQLOptions(spId);
5483
5642
  let checkoutId = this.state?.checkoutId;
5484
5643
  if (!checkoutId) {
5485
- const checkoutRes = await this.createCheckout(spId);
5644
+ const checkoutRes = await this.createCheckout(spId, "walley");
5486
5645
  checkoutId = checkoutRes?.id;
5487
5646
  if (checkoutId && this.state) {
5488
5647
  this.state = { ...this.state, checkoutId };
@@ -5551,7 +5710,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5551
5710
  if (this.state) this.state = { ...this.state, checkoutId };
5552
5711
  }
5553
5712
  if (!checkoutId) {
5554
- const checkoutRes = await this.createCheckout(spId);
5713
+ const checkoutRes = await this.createCheckout(spId, "nexi");
5555
5714
  checkoutId = checkoutRes?.id;
5556
5715
  if (checkoutId && this.state) {
5557
5716
  this.state = { ...this.state, checkoutId };
@@ -6116,7 +6275,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6116
6275
  return async (raw) => {
6117
6276
  let checkoutId = this.state?.checkoutId;
6118
6277
  if (!checkoutId) {
6119
- const created = await this.createCheckout(spId);
6278
+ const created = await this.createCheckout(spId, "apple-pay");
6120
6279
  checkoutId = created?.id ?? this.state?.checkoutId;
6121
6280
  }
6122
6281
  if (!checkoutId) {
@@ -6348,7 +6507,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6348
6507
  }
6349
6508
  let checkoutId = ctx.checkoutId ?? this.state.checkoutId;
6350
6509
  try {
6351
- const checkoutRes = await this.createCheckout(sponsorId);
6510
+ const checkoutRes = await this.createCheckout(sponsorId, "klarna");
6352
6511
  if (checkoutRes?.id) {
6353
6512
  checkoutId = checkoutRes.id;
6354
6513
  ctx.checkoutId = checkoutRes.id;
@@ -6400,6 +6559,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6400
6559
  if (nativeRes.checkout_id) {
6401
6560
  ctx.checkoutId = nativeRes.checkout_id;
6402
6561
  this.state = { ...this.state, checkoutId: nativeRes.checkout_id };
6562
+ this.announceCheckout(nativeRes.checkout_id, sponsorId, "klarna");
6403
6563
  }
6404
6564
  if (nativeRes.session_id) {
6405
6565
  ctx.sessionId = nativeRes.session_id;
@@ -6497,7 +6657,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6497
6657
  try {
6498
6658
  let checkoutId = ctx.checkoutId ?? this.state.checkoutId;
6499
6659
  try {
6500
- const checkoutRes = await this.createCheckout(sponsorId);
6660
+ const checkoutRes = await this.createCheckout(sponsorId, "klarna");
6501
6661
  if (checkoutRes?.id) {
6502
6662
  checkoutId = checkoutRes.id;
6503
6663
  ctx.checkoutId = checkoutRes.id;
@@ -6543,6 +6703,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
6543
6703
  if (nativeRes.checkout_id) {
6544
6704
  ctx.checkoutId = nativeRes.checkout_id;
6545
6705
  this.state = { ...this.state, checkoutId: nativeRes.checkout_id };
6706
+ this.announceCheckout(nativeRes.checkout_id, sponsorId, "klarna");
6546
6707
  }
6547
6708
  if (nativeRes.session_id) {
6548
6709
  ctx.sessionId = nativeRes.session_id;
@@ -7040,6 +7201,8 @@ var AnalyticsManager = class extends EventTarget {
7040
7201
  this.landing = null;
7041
7202
  /** Checkouts (`c:<id>`) and orders (`o:<number>`) this page load reported a `purchase` for: one each. */
7042
7203
  this.purchased = /* @__PURE__ */ new Set();
7204
+ /** Checkouts this page load reported an `add_payment_info` for: one each. */
7205
+ this.paymentInfoReported = /* @__PURE__ */ new Set();
7043
7206
  /** Purchases still waiting for Vio's order number: each sends at once, with what it has, when called. */
7044
7207
  this.pendingPurchases = /* @__PURE__ */ new Set();
7045
7208
  /** Between `pagehide` and `pageshow` (bfcache): nothing waits, it all goes out now. */
@@ -7107,7 +7270,7 @@ var AnalyticsManager = class extends EventTarget {
7107
7270
  this.teardown.push(() => window.removeEventListener("pageshow", onPageShow));
7108
7271
  if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
7109
7272
  const onVisibility = () => {
7110
- if (document.visibilityState !== "hidden" || this.pendingPurchases.size === 0) return;
7273
+ if (document.visibilityState !== "hidden") return;
7111
7274
  this.settlePendingPurchases();
7112
7275
  this.flushWithBeacon();
7113
7276
  };
@@ -7520,6 +7683,34 @@ var AnalyticsManager = class extends EventTarget {
7520
7683
  ...snap || buyerId !== void 0 ? { commerce: { ...snap ?? {}, ...buyer } } : {}
7521
7684
  });
7522
7685
  });
7686
+ on("vio:checkout-created", (ev) => {
7687
+ const d = ev.detail ?? {};
7688
+ const checkoutId = firstId(d.checkoutId);
7689
+ if (!checkoutId || this.paymentInfoReported.has(checkoutId)) return;
7690
+ this.paymentInfoReported.add(checkoutId);
7691
+ const sponsorId = toNumber(d.sponsorId);
7692
+ const base = { sponsorId, campaignId: campaignOf(d), slot: slotOf(d), ...contentOf(d) };
7693
+ const payment = {
7694
+ checkoutId,
7695
+ ...typeof d.paymentMethod === "string" && d.paymentMethod ? { paymentMethod: d.paymentMethod } : {}
7696
+ };
7697
+ const { slices, explicit } = this.cartSlices(sponsorId);
7698
+ if (slices.length > 1) {
7699
+ for (const slice of slices) {
7700
+ this.track("add_payment_info", {
7701
+ context: this.sliceContext(slice, base),
7702
+ commerce: { ...sliceCommerce(slice), ...payment },
7703
+ props: { slice: slice.slice, slices: slice.slices }
7704
+ });
7705
+ }
7706
+ } else {
7707
+ this.track("add_payment_info", {
7708
+ context: this.singleSliceContext(slices[0], explicit, base),
7709
+ commerce: { ...this.cartCommerce(sponsorId) ?? {}, ...payment }
7710
+ });
7711
+ }
7712
+ if (d.redirect === true) this.flushWithBeacon();
7713
+ });
7523
7714
  on("vio:payment-success", (ev) => {
7524
7715
  const d = ev.detail ?? {};
7525
7716
  const ids = purchaseIdsOf(d);
@@ -7600,6 +7791,7 @@ var AnalyticsManager = class extends EventTarget {
7600
7791
  }
7601
7792
  this.record(event);
7602
7793
  }
7794
+ this.sendNow();
7603
7795
  };
7604
7796
  const ask = ids.orderIdKind === "checkout" ? this.orderReferenceAsker(ids.checkoutId, sponsorId) : null;
7605
7797
  if (!ask) {
@@ -7608,7 +7800,6 @@ var AnalyticsManager = class extends EventTarget {
7608
7800
  }
7609
7801
  if (this.isPageHidden()) {
7610
7802
  send();
7611
- this.flushWithBeacon();
7612
7803
  return;
7613
7804
  }
7614
7805
  let done = false;
@@ -7622,6 +7813,14 @@ var AnalyticsManager = class extends EventTarget {
7622
7813
  this.pendingPurchases.add(settleNow);
7623
7814
  void waitForOrderNumber(ask, () => done).then(settle, () => settle());
7624
7815
  }
7816
+ /**
7817
+ * Sends the queue now, not at the next tick: by beacon when the page is
7818
+ * hidden or going away (a request may not survive it), else by fetch.
7819
+ */
7820
+ sendNow() {
7821
+ if (this.isPageHidden()) this.flushWithBeacon();
7822
+ else void this.flush();
7823
+ }
7625
7824
  /** Sends every purchase still waiting for its order number, at once, with what it has. */
7626
7825
  settlePendingPurchases() {
7627
7826
  for (const settle of [...this.pendingPurchases]) settle();
@@ -7957,6 +8156,7 @@ function pushGa4(event) {
7957
8156
  "add_to_cart",
7958
8157
  "view_cart",
7959
8158
  "begin_checkout",
8159
+ "add_payment_info",
7960
8160
  "purchase"
7961
8161
  ]);
7962
8162
  if (!commerceNames.has(event.name)) return;
@@ -7970,6 +8170,7 @@ function pushGa4(event) {
7970
8170
  currency: commerce.currency,
7971
8171
  value: commerce.value,
7972
8172
  transaction_id: commerce.order_id,
8173
+ payment_type: event.name === "add_payment_info" ? commerce.payment_method : void 0,
7973
8174
  items: commerce.items?.map(
7974
8175
  (i) => compact({
7975
8176
  item_id: i.product_id,
@@ -8794,5 +8995,5 @@ function vippsButtonAttributes(verb = "buy", opts = {}) {
8794
8995
  }
8795
8996
 
8796
8997
  export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, BUYER_REF_MAX_LENGTH, CHECKOUT_ATTRIBUTION_CAPS, CHECKOUT_SPONSOR_STORAGE_KEY, CLIENT_LANDING_URL_MAX_LENGTH, CONTENT_URL_MAX_LENGTH, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_CHECKOUT_WITH_ATTRIBUTION_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, GRAPHQL_INT_MAX, KUSTOM_RETURN_QUERY_KEYS, LINE_ORIGIN_CAPS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_ITEM_WITH_ORIGIN_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, activeCampaignEntry, addItem, adyenReturnOutcome, adyenReturnParams, autoSlotDisplayName, autoSlotId, buyerRefOf, campaignCandidates, capContentUrl, cartSlicesFrom, checkApplePayAvailability, checkoutAttributionOf, checkoutSponsorOf, componentKindOf, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, dominantCampaignOf, ensureVippsButton, entryContextOf, executeCartGraphQL, formatPrice, gatewayHas, gatewayHasBuyerRef, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCheckoutReference, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, isUnknownFeatureError, isUnknownTokenError, kustomReturnOutcome, landingUrl, lineOriginWire, lineOrigins, nexiReturnPaymentId, normalizeBuyerRef, normalizeOrigin, normalizeScope, normalizeSlotId, normalizeSlotKind, originAndPath, primaryImageUrl, purchaseFromPaymentSuccess, purchaseIdsOf, readNexiPending, rememberedCheckoutSponsor, resetGatewayFeatures, resolveCampaignId, resolveClientContext, resolveContent, resolveLanding, resolveSlotCampaign, sameOrigin, showApplePaySheet, sponsorCampaignEntry, sponsorIdentityFrom, storedLanding, toCampaignId, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes, withGatewayFeature };
8797
- //# sourceMappingURL=chunk-AFJ5M53F.js.map
8798
- //# sourceMappingURL=chunk-AFJ5M53F.js.map
8998
+ //# sourceMappingURL=chunk-AG4G2S2M.js.map
8999
+ //# sourceMappingURL=chunk-AG4G2S2M.js.map