@vio-live/web-sdk 0.29.0 → 0.32.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,177 @@ 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.32.0 — 2026-10-28
18
+
19
+ > First version published to npm since 0.29.0: **0.30.0 and 0.31.0 were never published on their own** — their
20
+ > changes (below) ship in this version too. A host on 0.29.0 should read the three sections.
21
+
22
+ ### Added
23
+ - **Options for the product detail** (README → *Options of the detail and the product cards*):
24
+ `gallery="carousel"` (every photo in one swipeable row with dots, no thumbnails),
25
+ `description-mode` (`preview` · `folded` · `open`), `highlights`, `delivery-time`,
26
+ `carrier-logos`; theme keys `detailGallery`, `detailCarouselPeek`, `detailDescriptionMode`,
27
+ `detailDescriptionPreviewLines`, `detailHighlightsDisplay`, `detailShippingReturnsDisplay`.
28
+ - **The brand's promises** under the buy zone (icon, title, line of text — up to three), from the
29
+ sponsor block's `highlights` (dashboard Settings → Brand) unless the host passes its own.
30
+ - **«Frakt & retur»** fold after «Beskrivelse»: one row per carrier (its logo from Commerce's
31
+ `logo_url`, or `carrier-logos`, or a line icon by the kind of delivery), how it delivers, the
32
+ price; the delivery time the brand wrote on the rate («Leveringstid 1-5 dager»); the right of
33
+ withdrawal and the brand's pages.
34
+ - **Product cards:** `<vio-product-carousel layout="list">` (one under the other),
35
+ `show-description` / `descriptions` (the catalogue's text or the editor's own per product),
36
+ `button-style` (`icon` · `bar` · `hidden`) and `button-label` (the Vev card's «Kjøp» bar); card
37
+ look through theme keys `card*` and `--vio-card-*`.
38
+ - `commerce.channel.product.getShippingRates` returns each rate's `description` and `logoUrl`.
39
+
40
+ ### Changed
41
+ - **The detail's order is the same on every screen**: sizes first in the buy zone, «Frakt fra …»
42
+ under them, stock on the name's line, «Beskrivelse» at the very bottom. The buy zone no longer
43
+ pins itself on desktop (`detailBuyPosition: 'sticky'` brings that back).
44
+ - **«Beskrivelse» shows a preview** (the first three lines and «Les mer») instead of being folded
45
+ shut. `description-mode="folded"` keeps the old behaviour.
46
+ - **The sheets close with a pull down from anywhere** while their content is at the top, not only
47
+ from the grab bar or the top bar (detail, cart, checkout).
48
+
49
+ ### ⚠️ Upgrade notes
50
+ - Rates are asked with `logo_url`; a gateway without the field is asked again without it once
51
+ (and not again on that page), so the rates never go missing over a logo.
52
+ - A host that styled `details.description-block` for «Beskrivelse» now gets
53
+ `.description-preview` (`part="description"` unchanged).
54
+
55
+ ## 0.31.0 — 2026-10-28
56
+
57
+ ### Added
58
+ - **Delivery and returns under the price in the product detail.** It reads «Frakt fra X · 14
59
+ dagers angrerett», where X is the product's cheapest shipping rate for the page's market (the
60
+ same `product_shipping` rates the backend cart offers). A cheapest rate of 0 reads «Gratis frakt».
61
+ With no known rate, only «14 dagers angrerett» is shown (`part="delivery-note"`). The rates come
62
+ from a separate query (`commerce.channel.product.getShippingRates`), so a failure there only
63
+ loses this line.
64
+ - **The cart drawer shows shipping and the total including it.** It uses the rate chosen on the
65
+ server cart, or the cheapest rate offered. Without a known rate it reads «Frakt: Beregnes i
66
+ kassen» and shows no total (`part="cart-shipping"`, `part="cart-total"`).
67
+ - **«Lagt i handlekurven» after «Legg i handlekurv».** The detail closes and a small toast offers
68
+ **«Til kassen»** and **«Fortsett å handle»** (`part="added-toast"`). «Til kassen» dispatches
69
+ `vio:checkout-open` for that brand, the same event the cart's «Til kassen» sends. It does nothing
70
+ when the cart was emptied in the meantime. The toast closes by itself after 8 s.
71
+ - **A line under «Kjøp nå med Vipps» in the product detail:** «Vipps fyller inn navn og adresse –
72
+ du godkjenner og betaler i appen» (`part="vipps-note"`). It is shown only when that button goes
73
+ straight to the app: Express is on for the brand and the brand's cart is empty. In other cases
74
+ the button opens the checkout first.
75
+ ### Changed
76
+ - **The product detail no longer picks a size for the shopper.** Only an option with a single
77
+ value is preselected. The chosen value is shown beside the option's name («Størrelse: M») and
78
+ on its pill. Every buy button (Vipps, «Legg i handlekurv», «Kjøp nå», Stripe, Klarna, Apple Pay)
79
+ checks for a missing choice first: it shows «Velg størrelse» (or «Velg farge», etc.) and adds or
80
+ pays nothing. «Få igjen — N stk» now counts only the variant actually chosen. Before a size is
81
+ chosen it says «På lager» if any size is in stock.
82
+ - **Two Vipps paths.** The product detail's «Kjøp nå med Vipps» stays **Express**. The
83
+ **cart's** Vipps button now opens the checkout with Vipps preselected. It no longer starts
84
+ Express or hands over to Vipps. **The checkout's Vipps is always the standard payment**, even
85
+ when the brand has Express on:
86
+ - the address form, the delivery and its price are required before paying, as for Stripe;
87
+ - the summary counts our shipping rate, so «Velges i Vipps» is gone;
88
+ - the payment is `startVippsPayment(sponsorId, form, { express: false })`, which now puts the
89
+ form's complete address on the checkout's shipping and billing address.
90
+
91
+ `COLLECTS_OWN_ADDRESS` no longer lists Vipps. See
92
+ [README → Two Vipps paths](README.md#the-hand-over-to-vipps-028).
93
+ - **«Mobilnummer» in the checkout's address form.** It is required only for the standard Vipps
94
+ payment, and optional for every other method; it never blocks their buttons or widgets.
95
+ - It accepts any market's number. A `+`/`00` prefix sets the calling code; without one, the
96
+ shipping country does (NO 47, SE 46, DK 45, FI 358, DE 49, GB 44…), falling back to the
97
+ page's country. A trunk 0 is dropped.
98
+ - A number that cannot be read shows «Skriv et gyldig mobilnummer…».
99
+ - It is sent as `phone` (national digits) + `phone_code` (digits, no plus) on the shipping and
100
+ billing address of every method that takes our form. `CheckoutAddress.phone` is new.
101
+ - **The checkout's Vipps refuses to start without the delivery saved.** If the chosen rate cannot
102
+ be saved on the cart, the Vipps or Stripe-link payment does not start and the checkout says
103
+ «Kunne ikke lagre fraktmetoden. Prøv igjen.». This covers a failed save and a rate without a
104
+ supplier. `startVippsPayment(…, { express: false })` also throws before creating anything when
105
+ the address is incomplete or has no usable mobile.
106
+
107
+ ### Fixed
108
+ - **«Velg størrelse» no mueve las tallas.** El aviso sale en la misma línea que el nombre de la opción, así que tocar comprar sin talla ya no desplaza los botones bajo el dedo.
109
+ - **No checkout and no payment for an empty cart or a total of 0 kr.** `Vio.checkout.open()`
110
+ throws for a cart that totals 0, as it already did for an empty one. Vipps (`startVippsPayment`,
111
+ `startVippsExpress`), the Stripe link, native Stripe and Adyen read the cart from the server and
112
+ refuse to create a checkout or a payment when it is empty or 0 kr. The buttons check the same
113
+ thing first: the cart's «Til kassen» is disabled, the checkout's pay buttons are disabled and
114
+ say «Handlekurven er tom.», and the detail shows no Vipps or Apple Pay button for a product at 0 kr.
115
+ - **Desktop: the product detail's pinned buy bar no longer covers the size picker.** The option
116
+ pickers now sit at the top of the buy zone (`part="buy-options"`), so they move with the
117
+ buttons whether the zone is static or sticky.
118
+ - **No buttons for payment methods the brand does not have while its methods load.** The
119
+ product detail and the cart drawer show a neutral placeholder (`part="buy-pending"` /
120
+ `.methods-pending`) until `getAvailablePaymentMethods` answers. They used to show every method,
121
+ so a Vipps-only brand flashed Stripe and Klarna. «Legg i handlekurv» and «Til kassen» are there
122
+ from the start. A failed lookup still shows every button, as before.
123
+ - **Storage never blocks add to cart or checkout (in-app browsers, private modes).** Every read
124
+ and write of `localStorage`/`sessionStorage` goes through one guard, `src/core/safe-storage.ts`.
125
+ A storage property that throws on access, a `null` storage or a refused write now falls back to
126
+ the page's memory. This covers the cart, the customer session id, the market's
127
+ currency/country/locale and the Klarna return. Before, `typeof localStorage` outside a `try`
128
+ could throw while the SDK started. Without storage, every browser also sent the same customer
129
+ session id, `cs_session_default`. It is now one id per page.
130
+
131
+ ### ⚠️ Upgrade notes
132
+ - The detail's buy buttons stay enabled before a size is chosen. Tapping one shows the prompt
133
+ instead of buying. A host that clicks `.add-to-cart` from a script needs a value in every option
134
+ first.
135
+ - A host's `vio:checkout-open` handler that calls `Vio.checkout.open()` must still catch the throw
136
+ (it already threw for an empty cart). It now also throws for a cart of 0 kr.
137
+ - Host CSS that placed `::part(option-group)` relative to `::part(buy)` should be checked: the
138
+ option groups are now inside `buy`.
139
+ - A host that already answers `vio:added-to-cart` with its own UI (for example, opening its cart
140
+ drawer) can turn the new toast off with `<vio-product-detail no-added-toast>`. The toast's
141
+ «Til kassen» relies on the host's `vio:checkout-open` handler, like the cart's does.
142
+ - **Backend (shopcart):** the checkout's Vipps payment is now `express: false` with the address
143
+ already on the checkout. shopcart must not request the Vipps profile on these standard
144
+ payments, and must carry the address `phone`/`phone_code` through to the order. Until that is
145
+ deployed, check a QA order to confirm the phone arrives.
146
+ - Hosts that pre-fill the checkout form through `Vio.checkout.setAddress()` should include
147
+ `phone`, or the shopper has to type it when paying with Vipps. Other methods do not need it.
148
+ - **The cart's Vipps button now opens the checkout through `vio:checkout-open`**, with
149
+ `paymentMethod: 'vipps'`. A host without a handler for that event loses direct Vipps from the
150
+ cart. The `vio:payment-redirect-*` events with `source: 'cart'` are gone; those with
151
+ `'detail'` and `'checkout'` remain.
152
+ - **`startVippsPayment(sponsorId, form, { express: false })` writes the full address**, including
153
+ the mobile, to the checkout's shipping and billing address before it creates the payment.
154
+ Without the flag, or with `express: true`, nothing changes.
155
+ - `Vio.checkout.open()` throws on a cart that totals 0, as well as on an empty one.
156
+ - The «Lagt i handlekurven» toast is on by default. Use `<vio-product-detail no-added-toast>` to
157
+ turn it off.
158
+
159
+ ## 0.30.0 — 2026-10-27
160
+
161
+ ### Added
162
+ - **Internal traffic** (ADR-0041): opening any page with **`?vio_internal=1`** marks the browser
163
+ (`localStorage`, `vio.internal.v1`) and `?vio_internal=0` clears it; read once at `Vio.init()`.
164
+ While marked, every analytics event carries **`client.internal: true`** and the collector's
165
+ reads leave it out. **`Vio.analytics.setInternal(boolean)`** / `Vio.analytics.isInternal` for
166
+ hosts; one `console.info` when marked. See
167
+ [README → Internal traffic](README.md#internal-traffic--mark-your-browser-as-a-tester-unreleased).
168
+
169
+ ### Changed
170
+ - **Stripe.js (~300 KB) is downloaded only for a brand that uses Stripe.** It used to load on
171
+ every page as soon as the SDK started, to have Apple Pay ready. Now it loads when the brand's
172
+ payment methods come back with a Stripe key (or when the host passes `stripePublishableKey`
173
+ to `Vio.init`). A brand that only takes Vipps never downloads it. Apple Pay still has it before
174
+ the tap: its button waits for the same key.
175
+ - **`component_impression` of `<vio-product-carousel>` carries `commerce.items`** — the products
176
+ shown, like its `view_item_list` — so the collector can count per-product impressions
177
+ (ADR-0041). The impression is observed once the products have loaded (the 1 s dwell starts
178
+ then); a failed fetch still counts it, without items. `Vio.analytics.observeImpression()` takes
179
+ a `commerce` getter, read when the impression fires. See
180
+ [README → Impressions carry the products shown](README.md#impressions-carry-the-products-shown-unreleased).
181
+ - Events never carry more than 100 `commerce.items` (the collector rejects the whole event above
182
+ its cap).
183
+
184
+ ### ⚠️ Upgrade notes
185
+ - `client.internal` needs the collector that knows the field; an older one strips it (the event is
186
+ kept, just not marked). Nothing to do on the host.
187
+
17
188
  ## 0.29.0 — 2026-10-24
18
189
 
19
190
  ### Added
package/README.md CHANGED
@@ -519,6 +519,49 @@ redirect }` — which is what analytics listens to; a host with its own sink can
519
519
  listen to it too. With `dataLayer: true` it is pushed as GA4's `add_payment_info`
520
520
  with `payment_type`.
521
521
 
522
+ ### Impressions carry the products shown (unreleased)
523
+
524
+ `<vio-product-carousel>`'s **`component_impression`** (≥50% visible for ≥1 s, once
525
+ per session and carousel) carries the products it shows in **`commerce.items`**
526
+ (`product_id`, `name`, `price`) and `commerce.currency` — the same lines as its
527
+ `view_item_list`. The collector counts the per-product "saw" step of the funnel
528
+ from them (ADR-0041). While the products load nothing is observed: the 1 s dwell
529
+ starts once they are on screen. A carousel whose fetch failed (slotted fallback)
530
+ still counts its impression, without items. A host calling
531
+ `Vio.analytics.observeImpression(el, context, { commerce: () => ({ items, currency }) })`
532
+ does the same for its own elements: the getter is read when the impression fires.
533
+
534
+ ### Internal traffic — mark your browser as a tester (unreleased)
535
+
536
+ Our own visits (testers, the team, agents) must not count in the dashboard. Mark
537
+ **each browser you test with** once:
538
+
539
+ 1. Open any page of the site that runs the SDK with **`?vio_internal=1`** added to
540
+ the URL — e.g. `https://example.com/article?vio_internal=1` (or
541
+ `…?utm_source=x&vio_internal=1` when the URL already has a query).
542
+ 2. Open the browser's console: the SDK says once
543
+ `[Vio.analytics] this browser is marked as internal traffic …`.
544
+ 3. That's it. The mark is remembered in this browser (`localStorage`,
545
+ `vio.internal.v1`, per site) for every page and every later visit, and every
546
+ analytics event it sends carries **`client.internal: true`**; the collector's
547
+ reads leave those events out.
548
+
549
+ To **undo** it, open any page with **`?vio_internal=0`**. Things to know:
550
+
551
+ - The mark is per browser and per site: Safari and Chrome on the same phone, a
552
+ private window, or another site are marked separately. Clearing the site's data
553
+ clears the mark.
554
+ - In-app browsers (Instagram, Facebook) and private windows may not keep
555
+ `localStorage`: there, only the page opened with `?vio_internal=1` is marked.
556
+ Prefer a regular browser for testing.
557
+ - The URL is read once per page load (at `Vio.init()`), not on client-side
558
+ navigation.
559
+ - The mark is the tester's own choice, not a tracking id: it is written even
560
+ before consent (`requireConsent`).
561
+ - A host can mark from code: **`Vio.analytics.setInternal(true | false)`**
562
+ (`Vio.analytics.isInternal` reads it). `resolveInternal()` / `INTERNAL_KEY` are
563
+ exported from `/core`.
564
+
522
565
  ## Sponsor identity — name and logo
523
566
 
524
567
  The elements say **who sells**: the sponsor's (brand's) display name and logo.
@@ -650,7 +693,7 @@ or on the element (`vio-product-detail { --vio-detail-logo-height: 48px; }`).
650
693
  | `detailOptionHeight` / `detailOptionGap` | `--vio-detail-option-height` / `--vio-detail-option-gap` | `36px` / `6px` | Size / option buttons (height is also their min width) |
651
694
  | `detailOptionRadius` / `detailOptionWrap` | `--vio-detail-option-radius` / `--vio-detail-option-wrap` | `0` / `wrap` | `nowrap` = one line that scrolls sideways |
652
695
  | `detailButtonHeight` / `detailButtonRadius` | `--vio-detail-button-height` / `--vio-detail-button-radius` | `52px` / `0` | Quantity stepper and «Legg i handlekurv» |
653
- | `detailBuyPosition` / `detailBuyShadow` | `--vio-detail-buy-position` / `--vio-detail-buy-shadow` | phones `static`, desktop `sticky` | Whether the buy zone (Vipps, quantity, add to cart) stays at the bottom of the panel while scrolling |
696
+ | `detailBuyPosition` / `detailBuyShadow` | `--vio-detail-buy-position` / `--vio-detail-buy-shadow` | `static` | `sticky` pins the buy zone (Vipps, quantity, add to cart) to the bottom of the panel while scrolling |
654
697
  | `cartWidth` / `checkoutWidth` | `--vio-cart-width` / `--vio-checkout-width` | `440px` | Drawer widths (desktop) |
655
698
  | `confirmLogoHeight` / `confirmTitleSize` / `confirmCheckBg` | `--vio-confirm-logo-height` / `--vio-confirm-title-size` / `--vio-confirm-check-bg` | `40px` / `24px` / green | The confirmation |
656
699
  | `redirectBg` / `redirectSpinner` | `--vio-redirect-bg` / `--vio-redirect-spinner` | translucent white / Vipps orange | The «Åpner Vipps …» overlay |
@@ -666,10 +709,14 @@ Also settable as CSS only: `--vio-detail-logo-max-width` (`220px`), `--vio-detai
666
709
  **`::part` names**, for what the properties do not cover:
667
710
 
668
711
  - `<vio-product-detail>`: `topbar`, `sponsor`, `close`, `gallery`, `image`, `thumbs`, `thumb`
669
- (+ `thumb-active`), `thumbs-arrow`, `info`, `brand`, `title`, `price`, `stock`,
670
- `option-group`, `option-label`, `options`, `option` (+ `option-selected`), `buy`,
671
- `buy-vipps`, `quantity-row`, `add-to-cart`, `seller-note`, `seller-terms`, `seller-refund`,
672
- `description`.
712
+ (+ `thumb-active`), `thumbs-arrow`, `info`, `brand`, `title`, `price`, `delivery-note`
713
+ (`delivery-shipping`, `delivery-withdrawal`), `stock`, `buy`, `buy-options`, `option-group`,
714
+ `option-label`, `option-chosen`, `option-prompt`, `options`, `option` (+ `option-selected`),
715
+ `buy-pending`, `buy-vipps`, `vipps-note`, `quantity-row`, `add-to-cart`, `seller-note`,
716
+ `seller-terms`, `seller-refund`, `description`, `highlights`, `highlight`, `shipping-returns`,
717
+ `slide`, `image-dots`, `carousel-arrow`, `added-toast` (`added-toast-checkout`,
718
+ `added-toast-continue`).
719
+ - `<vio-cart>`: `cart-shipping`, `cart-total`.
673
720
  - `<vio-checkout>`: `sponsor`, `confirmation`, `confirm-title`, `confirm-sponsor`, `confirm-details`.
674
721
 
675
722
  ```css
@@ -677,14 +724,51 @@ vio-product-detail::part(add-to-cart) { letter-spacing: 0; text-transform: none;
677
724
  vio-product-detail::part(option-selected) { background: #2b2b2b; }
678
725
  ```
679
726
 
680
- **The detail's order:** the selling brand top left; the main image; the thumbnails;
681
- name, price, stock, sizes; then, **on phones**, the buy zone — **Vipps first** (when the
682
- channel has it), quantity + «Legg i handlekurv», then the other ways to pay, and under them
683
- «Du handler direkte hos {brand}.» (the product's brand, never the article's) with links to
684
- the brand's «Kjøpsvilkår» and «Angrerett og retur» when it gave them (the sponsor block's
685
- `termsUrl` / `refundPolicyUrl`, https only, opened in a new tab) — and the
686
- description; **on desktop**, the description first and the buy zone pinned at the bottom
687
- of the panel. The description folds: «Beskrivelse» opens on a tap.
727
+ **The detail's order:** the selling brand top left; the images (thumbnails, or a carousel);
728
+ name and stock on one line, the price; the buy zone — the sizes (none preselected; every buy
729
+ button asks «Velg størrelse» first), «Frakt fra X · 14 dagers angrerett», **Vipps first** (when
730
+ the channel has it), quantity + «Legg i handlekurv», the other ways to pay, and under them «Du
731
+ handler direkte hos {brand}.» (the product's brand, never the article's) with links to the
732
+ brand's «Kjøpsvilkår» and «Angrerett og retur» when it gave them (the sponsor block's
733
+ `termsUrl` / `refundPolicyUrl`, https only, new tab); the brand's promises; «Beskrivelse»; and
734
+ «Frakt & retur». The same order on every screen.
735
+
736
+ ### Options of the detail and the product cards (0.32)
737
+
738
+ Every one is off or as before unless the host sets it: per element (attribute), or for the
739
+ whole page through `applyVioTheme` (theme key → custom property). The element wins.
740
+
741
+ **`<vio-product-detail>`**
742
+
743
+ | Attribute | Theme key | Values | What |
744
+ |---|---|---|---|
745
+ | `gallery` | `detailGallery` | `thumbs` (default) · `carousel` | `carousel`: every photo in one swipeable row with a dot per photo, no thumbnails — less height. `detailCarouselPeek` (`--vio-detail-carousel-peek`) shows the edge of the next one |
746
+ | `description-mode` | `detailDescriptionMode` | `preview` (default) · `folded` · `open` | «Beskrivelse»: the first lines fading out + «Les mer» (`detailDescriptionPreviewLines`, `3`); only the heading until a tap; or all of it |
747
+ | `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
+ | `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
+ | `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 |
750
+ | — | `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
+
752
+ The sheet closes with a pull down from its top bar, or from anywhere while its content is at the top.
753
+
754
+ **`<vio-product-carousel>`** (and its cards, `<vio-product>`)
755
+
756
+ | Attribute | Theme key | Values | What |
757
+ |---|---|---|---|
758
+ | `layout` | `carouselLayout` | `carousel` (default) · `list` | `list`: one card under the other, as wide as the column (`--vio-carousel-list-max-width`, `480px`) |
759
+ | `show-description` | — | boolean | Each card shows its product's description (`cardDescriptionLines`, `4`) |
760
+ | `descriptions` | — | `{ "<productId>": "text" }` (JSON) | The editor's own text for a product, over the catalogue's — shown even without `show-description`; `""` hides that one |
761
+ | `button-style` | — | `icon` (default) · `bar` · `hidden` | The buy button: the round bag on the image, a full-width «Kjøp» bar under the price (as the Vev card), or none. Both add to the cart, or open the product when it has sizes |
762
+ | `button-label` | — | text, default `Kjøp` | The bar's text («Kjøp · 2» once two are in the cart) |
763
+
764
+ Card look, by theme key (custom property): `cardFont`, `cardBorder`, `cardImageAspect`,
765
+ `cardImageFit`, `cardImageBg`, `cardImagePadding`, `cardMetaPadding`, `cardPriceFont`,
766
+ `cardAccent`, `cardBarRadius`, `cardDescriptionLines`, `cardRetailerOrder` (`-1` lifts the seller
767
+ line above the name). CSS only: `--vio-card-border-hover-color`, `--vio-card-add-size`,
768
+ `--vio-card-add-icon-size`, `--vio-card-brand-{size,tracking,weight,color,gap}`,
769
+ `--vio-card-name-{font,size,weight,gap}`, `--vio-card-price-{size,weight,gap}`,
770
+ `--vio-card-retailer-{display,size,tracking,weight,color,margin}`,
771
+ `--vio-card-description-{size,color,gap}`, `--vio-card-accent-text`.
688
772
 
689
773
  ### The hand-over to Vipps (0.28)
690
774
 
@@ -706,8 +790,17 @@ detail adds the product to the sponsor's cart and the checkout is built on that
706
790
  every line in it. It goes straight to Vipps only when the cart was **empty**; with lines
707
791
  already in it, the product is added and the checkout opens first (`vio:checkout-open`
708
792
  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
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
798
+ brand has Express on. Our form collects name, e-mail, **Mobilnummer**, address and the
799
+ delivery with its price before paying. The mobile is required for Vipps only and accepts any
800
+ market's number: the `+` prefix or the shipping country sets the calling code. The delivery is
801
+ saved on the cart first; if that fails, no payment starts. The payment goes out with
802
+ `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
711
804
  rule**: its sheet's amount is fixed before the tap, so it opens only on an empty cart, for the
712
805
  product × the chosen quantity; otherwise the checkout opens (`paymentMethod: 'apple-pay'`).
713
806