@vio-live/web-sdk 0.32.0 → 0.35.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,6 +14,129 @@ 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
+ ## 0.35.0 — 2026-10-29
18
+
19
+ > 0.34.0 was never published on its own: its changes (section order of the product detail and layout tracking,
20
+ > below) ship in this version too.
21
+
22
+ ### Added
23
+ - **`vippsCartPayment: 'express' | 'checkout'`** (theme key, `--vio-vipps-cart-payment`) and the
24
+ attribute **`vipps-cart-payment`** on `<vio-cart>` and `<vio-product-detail>`. `express` is the
25
+ default; `checkout` restores the 0.31–0.34 behaviour exactly. See
26
+ [README → The hand-over to Vipps](README.md#the-hand-over-to-vipps-028).
27
+
28
+ ### Changed
29
+ - **The Vipps buttons pay the sponsor's whole cart with Vipps Express** (Angelo, 2026-10-29: «el
30
+ botón negro va con el nuestro y el botón de Vipps paga el carrito entero»). The black «Til
31
+ kassen» is the way into our checkout; Vipps goes to Vipps.
32
+ - **`<vio-cart>`**: the Vipps button starts Express for the sponsor's whole cart — every line of
33
+ that sponsor's backend cart, never another sponsor's — the same Express as the detail: the
34
+ «Åpner Vipps …» overlay, `vio:payment-redirect-*` with `source: 'cart'`, `add_payment_info`
35
+ once, the return from Vipps and its confirmation. It no longer dispatches
36
+ `vio:checkout-open`. One payment at a time (the overlay takes every tap; a second tap while
37
+ one is starting does nothing).
38
+ - **`<vio-product-detail>`**: «Kjøp nå med Vipps» with lines already in the brand's cart adds the
39
+ product (size and quantity) and pays the **whole cart** with Express, instead of opening our
40
+ checkout. The line under the button says what will be paid before the tap: «Betaler hele
41
+ handlekurven · {n} varer · {total}» (n = every unit with this product at the chosen quantity,
42
+ total = the cart's subtotal with it; shipping is chosen in Vipps). It follows the cart and the
43
+ quantity. With an empty cart nothing changes.
44
+ - Express off for the brand, or a cart that cannot have it (several suppliers, no rate): the
45
+ checkout opens with Vipps preselected, as before.
46
+ - **A redirect payment (Vipps, Stripe link) reuses only its own sponsor's open checkout.** A
47
+ payment for one sponsor's cart started while another sponsor's checkout was open would have
48
+ gone out on that checkout; it now creates its own.
49
+
50
+ ### ⚠️ Upgrade notes
51
+ - **Hosts listening to `vio:checkout-open` from the cart's Vipps button no longer get it** by
52
+ default: the cart goes to Vipps directly. «Til kassen» still sends `vio:checkout-open`, and the
53
+ Vipps button sends it only when Express cannot start (then with `paymentMethod: 'vipps'`). To
54
+ keep the old flow, set `vipps-cart-payment="checkout"` or `applyVioTheme({ vippsCartPayment:
55
+ 'checkout' })`.
56
+ - **`vio:payment-redirect-start` / `-cancel` / `-error` with `source: 'cart'` are back** (gone
57
+ since 0.31). A host that draws its own loader (`preventDefault()` on the start) gets them from
58
+ the cart too.
59
+ - The same for the detail with lines in the cart: no `vio:checkout-open` any more, the hand-over
60
+ events (`source: 'detail'`) instead.
61
+ - **Backend:** nothing new. shopcart already builds Express on the checkout's cart, every line;
62
+ it refuses Express (`express: false`, no payment) for a cart of several suppliers, and the
63
+ checkout opens instead.
64
+
65
+ ## 0.34.0 — 2026-10-29
66
+
67
+ ### Added
68
+ - **Section order of `<vio-product-detail>` and layout tracking** (asked by Replit for Mote &
69
+ Livsstil). `Vio.configureDetail({ layout: { id, revision, sections, experimentId?, variantId? } })`
70
+ orders the sections below the gallery for the page (`{ layout: null }` resets; open details
71
+ re-render); the element's `layout` property / attribute (JSON) wins. Section ids: `brand`,
72
+ `title`, `stock`, `price`, `options`, `purchase`, `highlights`, `description`,
73
+ `shippingReturns`; unknown ids are ignored, duplicates keep the first, the essentials (`title`,
74
+ `price`, `options`, `purchase`) are appended when missing. The real DOM is reordered, the same
75
+ on every screen. `el.appliedLayout`, `Vio.detailLayout()` and the event
76
+ `vio:detail-layout-applied` say what was applied. See
77
+ [README → Section order and layout tracking](README.md#section-order-and-layout-tracking-034).
78
+ - **The layout on the funnel events.** `view_item` of a non-default layout carries `layout_id`,
79
+ `layout_revision`, `experiment_id`, `variant_id` in its `context`. The detail stores the layout
80
+ on the cart line origin (`CartOrigin.layoutId`, `layoutRevision`, `experimentId`, `variantId`),
81
+ and every item of `add_to_cart`, `remove_from_cart`, `view_cart`, `begin_checkout`,
82
+ `add_payment_info` and `purchase` carries `layout_id`, `layout_revision`, `experiment_id`,
83
+ `experiment_variant_id` (`""` when none; `items[].variant_id` stays the product variant). A line
84
+ with units from two layouts is two items. Nothing goes to Commerce.
85
+
86
+ ### ⚠️ Upgrade notes
87
+ - **No change without configuration:** the detail renders exactly as in 0.33 and `view_item`
88
+ sends nothing new. The only difference on the wire: the items of the cart events now always
89
+ carry the four layout keys, empty (`""`) when the units came from no layout — the collector
90
+ ignores keys it does not know yet.
91
+
92
+ ## 0.33.0 — 2026-10-28
93
+
94
+ ### Fixed
95
+ - **One cart per connection.** Every connection of a site (environment + key) shared one stored
96
+ cart, and every tab adopted what the others wrote: two tabs on two connections (Staging and Prod
97
+ side by side) could take each other's cart for a moment. Each connection now keeps its own
98
+ (`vio.cart.v1:<environment>:<hash of the key>` — never the key itself), a tab only listens to
99
+ its own, and `Vio.init` with another key switches `Vio.cart` to that connection's carts (the
100
+ first one's come back if the page switches back). An answer that arrives for the connection the
101
+ page left lands in that connection's cart, silently — never in the new one.
102
+ - **The commerce key is the channel of the article's campaign, not the sponsor's first block.**
103
+ With two campaigns of one sponsor on a surface (Careless: campaign 2 on channel 482, an internal
104
+ test campaign 1 on channel 480), products, carts and checkouts used the default campaign's
105
+ channel even for an article of the other. The key now follows the campaign in context (the
106
+ click's `campaignId`, `vio-campaign`, `data-vio-campaign`, `Vio.init`'s), among the sponsor's
107
+ blocks (a block with `commerce: null` is skipped instead of throwing). `Vio.commerceFor(sponsorId)`
108
+ still works and takes an optional `{ campaignId?, channelId? }`; commerce clients are kept per
109
+ sponsor and key. New: `Vio.sponsorCommerce(sponsorId, ctx?)`.
110
+ - **A cart remembers its channel.** `SponsorCartState.channelId` is stored when the backend cart is
111
+ created, and every later cart and checkout call for it (also the return from Vipps / Stripe,
112
+ through the per-checkout memory) uses that channel's key. If the bootstrap no longer lists the
113
+ channel, the stale `cartId` is dropped and the cart is created again on the current channel with
114
+ its lines — never a call with another channel's key. Carts from before keep the sponsor's first
115
+ block, as when they were created.
116
+ - **The order's campaign belongs to the cart's sponsor.** `attribution.campaign_id` on the checkout
117
+ and `campaignId` on `vio:checkout-created` (→ `add_payment_info`) took the page's campaign even
118
+ when it was another sponsor's. Now: the campaign the cart's lines were added from, else the
119
+ page's (`data-vio-campaign` / `vio-campaign` on the page, then `Vio.init`'s) only when the sponsor
120
+ takes part in it, else the slices' rule — only campaigns of that sponsor.
121
+ - **The bootstrap no longer lives for the whole page.** When the tab becomes visible again and the
122
+ cached bootstrap is older than 5 minutes, it is fetched again (the old one stays until the new
123
+ one arrives, and if the refresh fails). Carts keep their lines; an open checkout keeps paying
124
+ with its channel's key.
125
+ - **«Frakt & retur» no longer shows filler as the delivery time.** A rate's description equal to
126
+ its own name (dashboard-created rates copied it), `By Outshifter` or `Default Shipping Method`
127
+ (products' defaults) is no description in `getShippingRates`; the detail also drops one equal
128
+ to the product's title or the brand / seller name.
129
+
130
+ ### ⚠️ Upgrade notes
131
+ - **Hosts with several articles (Replit, Vev):** pass `sponsorId` **and** `campaignId` on
132
+ `vio:product-click`, set `data-vio-campaign` on each article's wrapper, and do **not** set a
133
+ site-wide `Vio.init({ campaignId })` — the campaign in context decides which channel sells.
134
+ - The carts kept in the old shared entry move, once, to the first connection that opens with none
135
+ of its own. Nothing to do.
136
+ - `Vio.cart` is the same object for the whole page, so listeners added to it keep working across a
137
+ re-init. A host that archived and restored carts per connection itself (Replit's
138
+ `vioCartConnections`) can rely on this instead.
139
+
17
140
  ## 0.32.0 — 2026-10-28
18
141
 
19
142
  > First version published to npm since 0.29.0: **0.30.0 and 0.31.0 were never published on their own** — their
package/README.md CHANGED
@@ -226,7 +226,7 @@ a campaign to a slot. But an editorial page — a Vev article, a Replit article
226
226
  often *is* part of one campaign, and the page can say so:
227
227
 
228
228
  ```ts
229
- Vio.init({ apiKey: '…', campaignId: 12 }) // the whole page
229
+ Vio.init({ apiKey: '…', campaignId: 12 }) // the whole page — only on a page of ONE campaign
230
230
  ```
231
231
 
232
232
  ```html
@@ -243,6 +243,19 @@ ancestors. On web/Replit a **per-article** campaign needs a per-article scope to
243
243
  same `carousel-1`, and the articles' campaigns would overwrite one another's
244
244
  assignment; Vev ids are per block, so it does not apply there. What it does:
245
245
 
246
+ - **Which channel sells.** One article = one campaign = one channel = one
247
+ sponsor, and one sponsor may run several campaigns on a surface, each on its own
248
+ commerce channel. The SDK reads products, creates the cart and pays with the key
249
+ of the channel of the **campaign in context** — the click's `campaignId`, the
250
+ element's `vio-campaign`, an ancestor's `data-vio-campaign`, then `Vio.init`'s —
251
+ among the blocks of that sponsor (`Vio.sponsorCommerce(sponsorId, { campaignId })`,
252
+ `Vio.commerceFor(sponsorId, { campaignId })`); without one, the sponsor's first
253
+ block, as before. A cart remembers the channel it was created on (`channelId`)
254
+ and every later cart and checkout call uses that channel's key; if the bootstrap
255
+ no longer lists that channel, the cart is created again on the current one with
256
+ its lines. So on a site with several articles (Replit, Vev), **set
257
+ `data-vio-campaign` per article and pass `sponsorId` and `campaignId` on
258
+ `vio:product-click`; do not set a site-wide `Vio.init({ campaignId })`.**
246
259
  - **Declaration.** The manifest carries the page's campaign (top-level) and an
247
260
  element's own campaign per location when it differs. The campaigns service
248
261
  (campaigns micro PR #16) turns that into assignments of the page's slots
@@ -372,9 +385,11 @@ Since 0.26 the origins do not stay in the browser: **Commerce receives them**
372
385
  - **Per checkout.** `CreateCheckout` and the `UpdateCheckout` right before paying
373
386
  carry `attribution`: `surface_key` (the SDK key — the gateway sets it from
374
387
  `X-API-Key` anyway, and shopcart checks it against the cart's channel, so a page
375
- cannot credit a sale to another surface), `campaign_id` (the page's campaign,
376
- `Vio.pageCampaign()` — else the campaign that explains most units of the cart under
377
- the same rule `purchase` is split by), `content_url` / `content_title` (the page at
388
+ cannot credit a sale to another surface), `campaign_id` (always a campaign the
389
+ cart's sponsor takes part in: the campaign the cart's lines were added from, else
390
+ the page's — `data-vio-campaign` / `vio-campaign` on the page, then `Vio.init`'s —
391
+ else the campaign that explains most units of the cart under the same rule
392
+ `purchase` is split by), `content_url` / `content_title` (the page at
378
393
  pay time) and `entry` (`{ referrer, utm_source, utm_medium, utm_campaign,
379
394
  landing_url }`: the landing the session **remembers** — the same memory the
380
395
  `client` block reads, which now keeps the landing page too — never the page the
@@ -697,6 +712,7 @@ or on the element (`vio-product-detail { --vio-detail-logo-height: 48px; }`).
697
712
  | `cartWidth` / `checkoutWidth` | `--vio-cart-width` / `--vio-checkout-width` | `440px` | Drawer widths (desktop) |
698
713
  | `confirmLogoHeight` / `confirmTitleSize` / `confirmCheckBg` | `--vio-confirm-logo-height` / `--vio-confirm-title-size` / `--vio-confirm-check-bg` | `40px` / `24px` / green | The confirmation |
699
714
  | `redirectBg` / `redirectSpinner` | `--vio-redirect-bg` / `--vio-redirect-spinner` | translucent white / Vipps orange | The «Åpner Vipps …» overlay |
715
+ | `vippsCartPayment` | `--vio-vipps-cart-payment` | `express` | What the Vipps button of `<vio-cart>` and `<vio-product-detail>` pays when the cart has lines: `express` = Vipps Express for the sponsor's whole cart; `checkout` = our checkout with Vipps preselected (0.31–0.34). See [The hand-over to Vipps](#the-hand-over-to-vipps-028) |
700
716
 
701
717
  Also settable as CSS only: `--vio-detail-logo-max-width` (`220px`), `--vio-detail-image-position`,
702
718
  `--vio-detail-image-radius`, `--vio-detail-thumb-fit` (`cover`), `--vio-detail-thumb-border-width`,
@@ -747,6 +763,7 @@ whole page through `applyVioTheme` (theme key → custom property). The element
747
763
  | `highlights` | — | `[{ icon, title, text }]` (JSON) | The brand's promises under the buy zone. By default the brand's own (dashboard Settings → Brand, sponsor block `highlights`); this list replaces them on the page. Icons: `plane heart shirt truck return leaf star shield mailbox home pin clock`, or an image URL. `detailHighlightsDisplay: 'none'` hides the strip |
748
764
  | `delivery-time` | — | text, e.g. `2-5 virkedager` | «Leveringstid …» in «Frakt & retur» for a brand whose rates do not say it (each rate's own text in Commerce — «Leveringstid 1-5 dager» — wins) |
749
765
  | `carrier-logos` | — | `{ "Bring": "https://…" }` (JSON) | Carrier logos by the name the rate starts with («Bring - Pakke i postkasse»), for rates without their own logo in Commerce (`logo_url`, which wins); otherwise a line icon by the kind of delivery |
766
+ | `vipps-cart-payment` | `vippsCartPayment` | `express` (default) · `checkout` | «Kjøp nå med Vipps» when the brand's cart already has lines: the product is added and the **whole cart** goes to Vipps Express, the line under the button saying «Betaler hele handlekurven · n varer · total»; or (`checkout`) the checkout opens first, Vipps preselected. Also on `<vio-cart>` (its Vipps button) |
750
767
  | — | `detailShippingReturnsDisplay` | `block` (default) · `none` | «Frakt & retur»: one row per carrier (logo or icon, carrier, how it delivers, price), the delivery time, the right of withdrawal and the brand's pages |
751
768
 
752
769
  The sheet closes with a pull down from its top bar, or from anywhere while its content is at the top.
@@ -770,6 +787,111 @@ line above the name). CSS only: `--vio-card-border-hover-color`, `--vio-card-add
770
787
  `--vio-card-retailer-{display,size,tracking,weight,color,margin}`,
771
788
  `--vio-card-description-{size,color,gap}`, `--vio-card-accent-text`.
772
789
 
790
+ ### Section order and layout tracking (0.34)
791
+
792
+ The sections of `<vio-product-detail>` **below the gallery** can be put in any order, and the
793
+ layout the shopper really saw travels with every event of the funnel — so a host can run an
794
+ experiment on the product page and read its conversion. The header (selling brand + close) and
795
+ the gallery stay where they are. Without any configuration nothing changes.
796
+
797
+ ```js
798
+ // The whole page (every detail open now and later):
799
+ Vio.configureDetail({
800
+ layout: {
801
+ id: 'pdp-sizes-first', // required
802
+ revision: '3',
803
+ sections: ['title', 'stock', 'price', 'options', 'purchase', 'description', 'shippingReturns'],
804
+ experimentId: 'pdp-2026-11', // optional
805
+ variantId: 'B', // optional — the experiment variant
806
+ },
807
+ })
808
+ Vio.configureDetail({ layout: null }) // back to the default
809
+
810
+ // One element (wins over the page): the property, or the attribute as JSON.
811
+ detail.layout = { id: 'pdp-b', revision: '1', sections: ['price', 'title', 'options', 'purchase'] }
812
+ // <vio-product-detail layout='{"id":"pdp-b","revision":"1","sections":["price","title","options","purchase"]}'>
813
+
814
+ detail.appliedLayout // what this detail applies
815
+ Vio.detailLayout() // what a detail without its own layout applies (the page's or the default)
816
+ ```
817
+
818
+ **Section ids**
819
+
820
+ | Id | What |
821
+ |---|---|
822
+ | `brand` | The product's brand line (`.brand`) |
823
+ | `title` | The product name |
824
+ | `stock` | «På lager» / «Få igjen» / «Utsolgt» |
825
+ | `price` | Price + compare-at price |
826
+ | `options` | The size / option groups and their prompt («Velg størrelse») |
827
+ | `purchase` | The SDK's buy group, never split: «Frakt fra…», Vipps / Apple Pay / the other ways to pay, quantity + «Legg i handlekurv», «Du handler direkte hos …» and the terms links |
828
+ | `highlights` | The brand's promises |
829
+ | `description` | «Beskrivelse» |
830
+ | `shippingReturns` | «Frakt & retur» |
831
+
832
+ Default order — exactly the DOM before 0.34: `brand, title, stock, price, options, purchase,
833
+ highlights, description, shippingReturns` (title and stock on one row; the options inside the
834
+ buy zone; highlights, description and «Frakt & retur» after it).
835
+
836
+ **Rules**
837
+
838
+ - Ids, `revision`, `experimentId`, `variantId`: strings of at most 64 characters of
839
+ `[A-Za-z0-9._:-]`. An invalid `id` (or `sections` that is not an array) ignores the layout, with
840
+ one `console.warn`; an invalid `revision` / `experimentId` / `variantId` is treated as absent.
841
+ - Unknown section ids are ignored; a duplicate: the first wins.
842
+ - Essentials — `title`, `price`, `options`, `purchase` — are appended in that order when missing.
843
+ Any other section left out is not rendered.
844
+ - `title` immediately followed by `stock` keep today's single row; otherwise each is its own block.
845
+ A run of `title` / `stock` / `price` is one `.headline` block.
846
+ - `options` immediately before `purchase` render inside the buy zone (`.buy`), as today — so a
847
+ sticky buy zone still holds them. Anywhere else they are their own block
848
+ (`.buy-options.options-apart`, same `part="buy-options"`), and the buy buttons still require the
849
+ choice: the same «Velg størrelse» prompt, and the options are scrolled into view.
850
+ - The real DOM is reordered (no CSS `order`), the same on phone and desktop. Every section after
851
+ `purchase` goes inside `.purchase`, as the description does by default: a sticky buy zone
852
+ (`detailBuyPosition: 'sticky'`) may slide over them, never over what comes before it.
853
+ `detailHighlightsDisplay`, `detailShippingReturnsDisplay`, `description-mode` and `gallery`
854
+ apply as before.
855
+
856
+ **Applied layout** — `el.appliedLayout`, `Vio.detailLayout()` and the event
857
+ `vio:detail-layout-applied` (bubbles, composed; React: `onLayoutApplied`), dispatched each time a
858
+ detail renders a product with a layout it was not yet shown with (every open, every change while
859
+ open; the detail also carries `productId` and `sponsorId`):
860
+
861
+ ```ts
862
+ {
863
+ id: string, revision: string, experimentId?: string, variantId?: string,
864
+ sections: string[], // what is rendered, in order
865
+ adjusted: Array<{ section: string, reason: 'unknown' | 'duplicate' | 'added-essential' }>,
866
+ source: 'element' | 'global' | 'default',
867
+ }
868
+ // no configuration:
869
+ { id: 'default', revision: '0', sections: [/* the default order */], adjusted: [], source: 'default' }
870
+ ```
871
+
872
+ **Tracking contract** (shared with the collector). Only a non-default layout (`source`
873
+ `element` / `global`) adds anything; the default one sends nothing new on `view_item`.
874
+
875
+ | Where | Fields | Value |
876
+ |---|---|---|
877
+ | `view_item` — `context` (next to `campaign_id`, `location_id`) | `layout_id`, `layout_revision`, `experiment_id`, `variant_id` | The layout the detail is shown with. `variant_id` here is the **experiment** variant (`context` has no product variant). Absent for the default layout; `experiment_id` / `variant_id` absent when the layout has none |
878
+ | Every item of `add_to_cart`, `remove_from_cart`, `view_cart`, `begin_checkout`, `add_payment_info`, `purchase` — `commerce.items[]` | `layout_id`, `layout_revision`, `experiment_id`, `experiment_variant_id` | The layout the units were added under (their cart line origin). Always present on these items: `""` when the units came from no layout (a card, the default layout). `items[].variant_id` stays the **product** variant — hence `experiment_variant_id` |
879
+
880
+ - The detail stores the layout on the cart line origin when it adds to the cart (any buy button):
881
+ `CartOrigin.layoutId`, `layoutRevision`, `experimentId`, `variantId` (the experiment variant),
882
+ taken from the layout shown at that moment. They persist with the cart (`localStorage`), so they
883
+ survive the return from Vipps / Stripe, a reload and another tab; changing the page's layout
884
+ later never rewrites lines already in the cart.
885
+ - A line whose units came from several layouts is split per layout into several items (each with
886
+ its quantity), the same way the units are split per campaign / slot (`Vio.cartSlices()` items
887
+ carry `layoutId`, `layoutRevision`, `experimentId`, `experimentVariantId` when they have a
888
+ layout). A purchase is never credited whole to the last product opened.
889
+ - `vio:added-to-cart` from the detail carries `layout: { layoutId, layoutRevision, experimentId?,
890
+ variantId? }` for a non-default layout (`variantId` of the event itself stays the product's).
891
+ - Internal traffic stays excluded as before; campaign, sponsor, channel, content and slot are
892
+ unchanged. The layout is **not** sent to Commerce (the line origin's wire to shopcart has no
893
+ layout fields): the collector joins `order_paid` to these events by `checkout_id`.
894
+
773
895
  ### The hand-over to Vipps (0.28)
774
896
 
775
897
  Between the tap on Vipps (detail, cart or checkout) and the Vipps page, the SDK shows
@@ -785,24 +907,41 @@ tap, so a second payment cannot start. Events on `document`:
785
907
  `source` is `'detail'`, `'cart'` or `'checkout'`. The overlay is `.vio-redirect-overlay`
786
908
  in the light DOM.
787
909
 
788
- **The detail's Vipps button pays the whole cart.** «Kjøp nå med Vipps» on the product
789
- detail adds the product to the sponsor's cart and the checkout is built on that cart —
790
- every line in it. It goes straight to Vipps only when the cart was **empty**; with lines
791
- already in it, the product is added and the checkout opens first (`vio:checkout-open`
792
- with `paymentMethod: 'vipps'`, no hand-over events), every line and the total on screen,
793
- so what the Vipps page lists is what the shopper saw.
794
-
795
- **Two Vipps paths (2026-10-28).** The detail's «Kjøp nå med Vipps» is **Express**: the app
796
- fills in name and address. The **cart's** Vipps button opens the checkout with Vipps
797
- preselected, and the checkout's Vipps is always the **standard** payment, even when the
910
+ **The Vipps buttons pay the sponsor's whole cart with Express (2026-10-29).** The black
911
+ «Til kassen» in `<vio-cart>` is the way into our checkout; the Vipps buttons go to Vipps:
912
+
913
+ - **`<vio-cart>`**: the Vipps button starts **Vipps Express for the whole cart** of the
914
+ sponsor (every line of its backend cart, never another sponsor's) — the hand-over above with
915
+ `source: 'cart'`, `add_payment_info` once, and the return from Vipps and its confirmation
916
+ exactly as from the detail. No `vio:checkout-open`.
917
+ - **`<vio-product-detail>`**: «Kjøp nå med Vipps» adds the product (size, quantity) and starts
918
+ Express for the sponsor's cart. With an **empty** cart that is the product alone (the line
919
+ under the button: «Vipps fyller inn navn og adresse – du godkjenner og betaler i appen»). With
920
+ **lines already in it**, the whole cart is paid, and the line under the button says so before
921
+ the tap: «Betaler hele handlekurven · {n} varer · {total}» — n = every unit including this
922
+ product at the chosen quantity, total = the cart's subtotal with it (shipping is chosen in
923
+ Vipps).
924
+
925
+ When the brand has Express off, or the cart cannot have it (several suppliers, digital goods,
926
+ no rate for the country — shopcart answers `express: false` and creates nothing), the checkout
927
+ opens with Vipps preselected (`vio:payment-redirect-cancel` with `'express-off'` /
928
+ `'not-started'`, then `vio:checkout-open`).
929
+
930
+ **The option: `vipps-cart-payment` / `vippsCartPayment`.** `express` is the default.
931
+ `checkout` brings back 0.31–0.34 exactly: the cart's Vipps button opens the checkout with Vipps
932
+ preselected (`vio:checkout-open`, `paymentMethod: 'vipps'`, no hand-over), and the detail's
933
+ does the same when the cart already has lines (product added first); an empty cart is Express
934
+ either way.
935
+
936
+ **The checkout's Vipps is the standard payment** (2026-10-28), even when the
798
937
  brand has Express on. Our form collects name, e-mail, **Mobilnummer**, address and the
799
938
  delivery with its price before paying. The mobile is required for Vipps only and accepts any
800
939
  market's number: the `+` prefix or the shipping country sets the calling code. The delivery is
801
940
  saved on the cart first; if that fails, no payment starts. The payment goes out with
802
941
  `express: false`, and the address (with `phone` + `phone_code`) is on the checkout's shipping
803
- and billing address. **Apple Pay on the detail follows the same
804
- rule**: its sheet's amount is fixed before the tap, so it opens only on an empty cart, for the
805
- product × the chosen quantity; otherwise the checkout opens (`paymentMethod: 'apple-pay'`).
942
+ and billing address. **Apple Pay on the detail** has its sheet's amount fixed before the tap,
943
+ so it opens only on an empty cart, for the product × the chosen quantity; otherwise the
944
+ checkout opens (`paymentMethod: 'apple-pay'`).
806
945
 
807
946
  ### What the shopper pays is what the page shows — the cart, the server, the other tabs
808
947
 
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=chunk-3YFJFDSN.js.map
3
+ //# sourceMappingURL=chunk-3YFJFDSN.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-QGJUBVH3.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-3YFJFDSN.js"}