@vio-live/web-sdk 0.33.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.
@@ -1,4 +1,4 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, shippingDescriptionOf, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-ZORYG4W3.js';
1
+ import { Vio, __decorateClass, primaryImageUrl, displayPrice, resolveDetailLayout, toCampaignId, getGlobalCurrency, copyAppliedLayout, onGlobalDetailLayoutChange, layoutSignature, layoutOriginOf, ensureVippsButton, formatPrice, shippingDescriptionOf, buyerRefOf, nexiReturnPaymentId, readNexiPending, kustomReturnOutcome, KUSTOM_RETURN_QUERY_KEYS, storageGet, ADYEN_RETURN_METHOD, adyenReturnParams, ADYEN_RETURN_QUERY_KEYS, adyenReturnOutcome, getGlobalCountryCode, isValidPhone, gatewayHasBuyerRef, rememberedCheckoutSponsor, checkoutSponsorOf, AdyenOriginError, normalizeSlotId, autoSlotDisplayName, autoSlotId, sponsorIdentityFrom, Configuration, normalizeScope, campaignCandidates } from './chunk-QA5R4VYF.js';
2
2
  import { LitElement, css, html, nothing } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -201,7 +201,8 @@ var THEME_KEY_TO_CSS_VAR = {
201
201
  confirmTitleSize: "--vio-confirm-title-size",
202
202
  confirmCheckBg: "--vio-confirm-check-bg",
203
203
  redirectBg: "--vio-redirect-bg",
204
- redirectSpinner: "--vio-redirect-spinner"
204
+ redirectSpinner: "--vio-redirect-spinner",
205
+ vippsCartPayment: "--vio-vipps-cart-payment"
205
206
  };
206
207
  function applyVioTheme(overrides, target = typeof document !== "undefined" ? document.documentElement : null) {
207
208
  if (!target) return;
@@ -1556,6 +1557,21 @@ function returnCancelled(detail) {
1556
1557
  emit("vio:payment-redirect-cancel", { ...detail, reason: "cancelled" });
1557
1558
  }
1558
1559
 
1560
+ // src/ui/vipps-cart-payment.ts
1561
+ var VIPPS_CART_PAYMENT_VAR = "--vio-vipps-cart-payment";
1562
+ function resolveVippsCartPayment(el, own) {
1563
+ let fromTheme = "";
1564
+ if (!own) {
1565
+ try {
1566
+ fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(el).getPropertyValue(VIPPS_CART_PAYMENT_VAR) : "";
1567
+ } catch {
1568
+ fromTheme = "";
1569
+ }
1570
+ }
1571
+ const value = String(own || fromTheme || "").trim().replace(/^["']|["']$/g, "").toLowerCase();
1572
+ return value === "checkout" ? "checkout" : "express";
1573
+ }
1574
+
1559
1575
  // src/core/checkout/method-taxonomy.ts
1560
1576
  function normalizeMethodName(name) {
1561
1577
  return String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
@@ -2037,6 +2053,19 @@ function unlockPageScroll() {
2037
2053
  }
2038
2054
 
2039
2055
  // src/ui/components/vio-product-detail.ts
2056
+ var layoutAttribute = {
2057
+ fromAttribute(value) {
2058
+ if (value === null || value.trim() === "") return null;
2059
+ try {
2060
+ return JSON.parse(value);
2061
+ } catch {
2062
+ return value;
2063
+ }
2064
+ },
2065
+ toAttribute(value) {
2066
+ return value === null || value === void 0 ? null : JSON.stringify(value);
2067
+ }
2068
+ };
2040
2069
  var ADDED_TOAST_MS = 8e3;
2041
2070
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
2042
2071
  '<svg width="360" height="150" viewBox="0 0 360 150" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M360 77.4001C360 51.8001 347.6 31.6001 323.9 31.6001C300.1 31.6001 285.7 51.8001 285.7 77.2001C285.7 107.3 302.7 122.5 327.1 122.5C339 122.5 348 119.8 354.8 116V96.0001C348 99.4001 340.2 101.5 330.3 101.5C320.6 101.5 312 98.1001 310.9 86.3001H359.8C359.8 85.0001 360 79.8001 360 77.4001ZM310.6 67.9001C310.6 56.6001 317.5 51.9001 323.8 51.9001C329.9 51.9001 336.4 56.6001 336.4 67.9001H310.6Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M247.1 31.6001C237.3 31.6001 231 36.2001 227.5 39.4001L226.2 33.2001H204.2V149.8L229.2 144.5L229.3 116.2C232.9 118.8 238.2 122.5 247 122.5C264.9 122.5 281.2 108.1 281.2 76.4001C281.1 47.4001 264.6 31.6001 247.1 31.6001ZM241.1 100.5C235.2 100.5 231.7 98.4001 229.3 95.8001L229.2 58.7001C231.8 55.8001 235.4 53.8001 241.1 53.8001C250.2 53.8001 256.5 64.0001 256.5 77.1001C256.5 90.5001 250.3 100.5 241.1 100.5Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.8 25.7L194.9 20.3V0L169.8 5.3V25.7Z" fill="#533AFD"/><path d="M194.9 33.3H169.8V120.8H194.9V33.3Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M142.9 40.7L141.3 33.3H119.7V120.8H144.7V61.5C150.6 53.8 160.6 55.2 163.7 56.3V33.3C160.5 32.1 148.8 29.9 142.9 40.7Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M92.8999 11.6001L68.4999 16.8001L68.3999 96.9001C68.3999 111.7 79.4999 122.6 94.2999 122.6C102.5 122.6 108.5 121.1 111.8 119.3V99.0001C108.6 100.3 92.7999 104.9 92.7999 90.1001V54.6001H111.8V33.3001H92.7999L92.8999 11.6001Z" fill="#533AFD"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25.3 58.7001C25.3 54.8001 28.5 53.3001 33.8 53.3001C41.4 53.3001 51 55.6001 58.6 59.7001V36.2001C50.3 32.9001 42.1 31.6001 33.8 31.6001C13.5 31.6001 0 42.2001 0 59.9001C0 87.5001 38 83.1001 38 95.0001C38 99.6001 34 101.1 28.4 101.1C20.1 101.1 9.5 97.7001 1.1 93.1001V116.9C10.4 120.9 19.8 122.6 28.4 122.6C49.2 122.6 63.5 112.3 63.5 94.4001C63.4 64.6001 25.3 69.9001 25.3 58.7001Z" fill="#533AFD"/></svg>'
@@ -2087,11 +2116,18 @@ var VioProductDetail = class extends LitElement {
2087
2116
  this.vioContentId = "";
2088
2117
  this.vioContentTitle = "";
2089
2118
  this.noAddedToast = false;
2119
+ this.vippsCartPayment = "";
2090
2120
  this.gallery = "";
2091
2121
  this.descriptionMode = "";
2092
2122
  this.highlights = [];
2093
2123
  this.deliveryTime = "";
2094
2124
  this.carrierLogos = {};
2125
+ this.layout = null;
2126
+ /** The layout the detail renders with now (resolved before each render). */
2127
+ this.layoutNow = resolveDetailLayout(null);
2128
+ /** The product and layout `vio:detail-layout-applied` was last said for. */
2129
+ this.appliedFor = null;
2130
+ this.unsubscribeLayout = null;
2095
2131
  this.slotRegistration = new SlotRegistration(this, "detail");
2096
2132
  /** What the click that opened the detail carried ('' when nothing), and for which product. */
2097
2133
  this.eventSlot = "";
@@ -2180,6 +2216,10 @@ var VioProductDetail = class extends LitElement {
2180
2216
  this.show();
2181
2217
  void this.fetchProduct();
2182
2218
  };
2219
+ /** The cart changed (another tab, the drawer): the line under Vipps counts it again. */
2220
+ this.boundCartChange = () => {
2221
+ if (this.open) this.requestUpdate();
2222
+ };
2183
2223
  /** The photo in view is the active one (dots, Apple Pay sheet, events). */
2184
2224
  this.onCarouselScroll = () => {
2185
2225
  const track = this.carouselTrack();
@@ -2198,6 +2238,14 @@ var VioProductDetail = class extends LitElement {
2198
2238
  if (atEnd !== this.thumbsAtEnd) this.thumbsAtEnd = atEnd;
2199
2239
  };
2200
2240
  }
2241
+ /**
2242
+ * The layout this detail applies (0.34): `{ id, revision, experimentId?,
2243
+ * variantId?, sections, adjusted, source }` — `source` is `element` (its own
2244
+ * `layout`), `global` (`Vio.configureDetail`) or `default`. A copy.
2245
+ */
2246
+ get appliedLayout() {
2247
+ return copyAppliedLayout(resolveDetailLayout(this.layout));
2248
+ }
2201
2249
  /** The card's context applies to the product it opened — not to one the host opened by hand later. */
2202
2250
  get fromCard() {
2203
2251
  return this.eventProductId !== "" && this.eventProductId === String(this.productId);
@@ -2714,6 +2762,10 @@ var VioProductDetail = class extends LitElement {
2714
2762
  }
2715
2763
  /* The sizes follow the price closely: they are the next thing to do. */
2716
2764
  .headline + .purchase { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
2765
+ /* A layout with the sizes apart from the buy zone (0.34): they follow the
2766
+ price closely instead, and the buy zone keeps the normal gap. */
2767
+ .headline + .buy-options.options-apart { margin-top: calc(var(--vio-detail-sizes-gap, 2px) - var(--vio-detail-info-gap, 20px)); }
2768
+ .headline + .purchase.options-apart { margin-top: 0; }
2717
2769
  .price {
2718
2770
  font-size: var(--vio-detail-price-size, 24px);
2719
2771
  font-weight: var(--vio-detail-price-weight, 700);
@@ -3205,6 +3257,9 @@ var VioProductDetail = class extends LitElement {
3205
3257
  }
3206
3258
  connectedCallback() {
3207
3259
  super.connectedCallback();
3260
+ Vio.cart.addEventListener("change", this.boundCartChange);
3261
+ this.unsubscribeLayout?.();
3262
+ this.unsubscribeLayout = onGlobalDetailLayoutChange(() => this.requestUpdate());
3208
3263
  this.slotRegistration.connect(this.slotAttributes());
3209
3264
  void this.loadVippsButton();
3210
3265
  document.addEventListener("vio:product-click", this.boundProductClick);
@@ -3213,8 +3268,11 @@ var VioProductDetail = class extends LitElement {
3213
3268
  if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
3214
3269
  this.addedToastTimer = null;
3215
3270
  document.removeEventListener("vio:product-click", this.boundProductClick);
3271
+ Vio.cart.removeEventListener("change", this.boundCartChange);
3216
3272
  this.slotRegistration.clear();
3217
3273
  this.bottomSheet.disconnect();
3274
+ this.unsubscribeLayout?.();
3275
+ this.unsubscribeLayout = null;
3218
3276
  super.disconnectedCallback();
3219
3277
  }
3220
3278
  updated(changed) {
@@ -3238,17 +3296,38 @@ var VioProductDetail = class extends LitElement {
3238
3296
  else this.revealActiveSlide();
3239
3297
  }
3240
3298
  this.measureLogo();
3299
+ this.announceLayout();
3241
3300
  if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
3242
3301
  this.slotRegistration.sync(this.slotAttributes());
3243
3302
  }
3244
3303
  }
3245
3304
  willUpdate(changed) {
3305
+ this.layoutNow = resolveDetailLayout(this.layout);
3246
3306
  if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
3247
3307
  if (changed.has("descriptionMode") || changed.has("open") && this.open) {
3248
3308
  const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
3249
3309
  this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
3250
3310
  }
3251
3311
  }
3312
+ /**
3313
+ * `vio:detail-layout-applied` (bubbles, composed), with the applied layout,
3314
+ * each time a product renders with a layout it has not been shown with yet:
3315
+ * every open of a product, and every change of layout while it is open.
3316
+ */
3317
+ announceLayout() {
3318
+ const product = this.product;
3319
+ if (!product || this.fetchError) return;
3320
+ const signature = layoutSignature(this.layoutNow);
3321
+ if (this.appliedFor && this.appliedFor.product === product && this.appliedFor.signature === signature) return;
3322
+ this.appliedFor = { product, signature };
3323
+ this.dispatchEvent(
3324
+ new CustomEvent("vio:detail-layout-applied", {
3325
+ bubbles: true,
3326
+ composed: true,
3327
+ detail: { ...copyAppliedLayout(this.layoutNow), productId: product.id, sponsorId: this.sponsorId }
3328
+ })
3329
+ );
3330
+ }
3252
3331
  /** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
3253
3332
  themeChoice(own, cssVar) {
3254
3333
  const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
@@ -3378,10 +3457,17 @@ var VioProductDetail = class extends LitElement {
3378
3457
  const price = Number(
3379
3458
  this.product.price?.amount_incl_taxes ?? this.product.price?.amount
3380
3459
  );
3460
+ const shown = layoutOriginOf(resolveDetailLayout(this.layout));
3381
3461
  Vio.analytics.track("view_item", {
3382
3462
  context: {
3383
3463
  sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
3384
- ...this.attribution()
3464
+ ...this.attribution(),
3465
+ ...shown.layoutId ? {
3466
+ layoutId: shown.layoutId,
3467
+ layoutRevision: shown.layoutRevision ?? "",
3468
+ experimentId: shown.experimentId,
3469
+ variantId: shown.variantId
3470
+ } : {}
3385
3471
  },
3386
3472
  commerce: {
3387
3473
  items: [
@@ -3712,7 +3798,8 @@ var VioProductDetail = class extends LitElement {
3712
3798
  this.selectionPrompt = missing.name;
3713
3799
  void this.updateComplete.then(() => {
3714
3800
  const group = this.renderRoot.querySelector(".option-group.needs-choice");
3715
- group?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
3801
+ const apart = !!group?.closest(".options-apart");
3802
+ group?.scrollIntoView?.({ behavior: "smooth", block: apart ? "center" : "nearest" });
3716
3803
  });
3717
3804
  return false;
3718
3805
  }
@@ -3782,13 +3869,14 @@ var VioProductDetail = class extends LitElement {
3782
3869
  const variant = this.selectedVariant();
3783
3870
  const sponsorName = resolveSponsorIdentity(this.sponsorId)?.name || void 0;
3784
3871
  const { campaignId, slot, contentId, componentTemplateId, contentUrl, contentTitle } = this.attribution();
3872
+ const layout = layoutOriginOf(this.layoutNow);
3785
3873
  Vio.cart.addProduct({
3786
3874
  product: this.product,
3787
3875
  sponsorId: this.sponsorId,
3788
3876
  sponsorName,
3789
3877
  quantity: this.quantity,
3790
3878
  variantId: variant?.id,
3791
- origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle }
3879
+ origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle, ...layout }
3792
3880
  });
3793
3881
  return variant?.id;
3794
3882
  }
@@ -3807,7 +3895,9 @@ var VioProductDetail = class extends LitElement {
3807
3895
  sponsorId: this.sponsorId,
3808
3896
  quantity: this.quantity,
3809
3897
  variantId,
3810
- ...this.attribution()
3898
+ ...this.attribution(),
3899
+ // The detail layout of the add (0.34) — `variantId` above is the product's.
3900
+ ...this.layoutNow.source !== "default" ? { layout: layoutOriginOf(this.layoutNow) } : {}
3811
3901
  }
3812
3902
  })
3813
3903
  );
@@ -3920,7 +4010,7 @@ var VioProductDetail = class extends LitElement {
3920
4010
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3921
4011
  if (!this.requireSelection()) return;
3922
4012
  if (!(this.unitPrice > 0)) return;
3923
- if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
4013
+ if (this.cartHasLines() && this.vippsCartMode() === "checkout") {
3924
4014
  this.addCurrentToCart();
3925
4015
  this.close();
3926
4016
  this.openCheckoutWithVipps();
@@ -3932,12 +4022,46 @@ var VioProductDetail = class extends LitElement {
3932
4022
  void this.startVippsExpressOrCheckout(redirect);
3933
4023
  }
3934
4024
  /**
3935
- * The Vipps button will open the app with Express (see buyWithVipps): the
3936
- * brand has Express on, and this brand's cart is empty — with lines in it
3937
- * the checkout opens first.
4025
+ * The Vipps button will open the app with Express for this product alone
4026
+ * (see buyWithVipps): the brand has Express on, and this brand's cart is
4027
+ * empty.
3938
4028
  */
3939
4029
  vippsGoesStraightToApp() {
3940
- return this.vippsExpress && (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) === 0;
4030
+ return this.vippsExpress && !this.cartHasLines();
4031
+ }
4032
+ /** This brand's cart already has lines. */
4033
+ cartHasLines() {
4034
+ return (Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0;
4035
+ }
4036
+ /** `vipps-cart-payment`, else the theme's, else `express`. */
4037
+ vippsCartMode() {
4038
+ return resolveVippsCartPayment(this, this.vippsCartPayment);
4039
+ }
4040
+ /**
4041
+ * What the Vipps button will pay when the brand's cart already has lines
4042
+ * (`express` mode): every unit in the cart plus this product at the chosen
4043
+ * quantity, and their total, as the cart sums it (shipping is chosen in
4044
+ * Vipps). null with an empty cart, or in `checkout` mode.
4045
+ */
4046
+ wholeCartToPay() {
4047
+ if (this.vippsCartMode() !== "express") return null;
4048
+ const cart = Vio.cart.getCart(this.sponsorId);
4049
+ const items = cart?.items ?? [];
4050
+ if (items.length === 0) return null;
4051
+ const units = items.reduce((n, i) => n + (Number(i.quantity) || 0), 0) + this.quantity;
4052
+ const total = items.reduce((sum, i) => sum + (Number(i.unitPrice) || 0) * (Number(i.quantity) || 0), 0) + this.unitPrice * this.quantity;
4053
+ return { units, total, currency: cart?.currency || this.currency };
4054
+ }
4055
+ /** The line under the Vipps button: what is paid (a cart with lines) or what happens in the app (Express, empty cart). */
4056
+ renderVippsNote() {
4057
+ const whole = this.wholeCartToPay();
4058
+ if (whole) {
4059
+ return html`<p class="vipps-note vipps-note-cart" part="vipps-note">Betaler hele handlekurven · ${whole.units} varer · ${formatPrice(whole.total, whole.currency)}</p>`;
4060
+ }
4061
+ if (this.vippsGoesStraightToApp()) {
4062
+ return html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>`;
4063
+ }
4064
+ return "";
3941
4065
  }
3942
4066
  async startVippsExpressOrCheckout(redirect) {
3943
4067
  let fallback = true;
@@ -4012,8 +4136,8 @@ var VioProductDetail = class extends LitElement {
4012
4136
  * after an await, so it cannot wait for the server's cart), while the
4013
4137
  * charge is the sponsor's WHOLE cart. So the sheet opens only when it shows
4014
4138
  * what will be charged: an empty cart, and a sheet prepared for this
4015
- * product × quantity. Otherwise — lines already in the cart (as with Vipps,
4016
- * see buyWithVipps), or a quantity whose sheet is still being prepared —
4139
+ * product × quantity. Otherwise — lines already in the cart, or a quantity
4140
+ * whose sheet is still being prepared —
4017
4141
  * the product is added and the checkout opens with Apple Pay preselected:
4018
4142
  * every line and the total on screen, and its own Apple Pay button pays
4019
4143
  * that total.
@@ -4318,9 +4442,6 @@ var VioProductDetail = class extends LitElement {
4318
4442
  }
4319
4443
  if (!this.product) return html``;
4320
4444
  const p = this.product;
4321
- const stock = this.stockLabel();
4322
- const compare = this.compareAt;
4323
- const showCompare = compare !== null && compare > this.unitPrice;
4324
4445
  const images = this.allImages();
4325
4446
  const activeImage = this.activeImage;
4326
4447
  return html`
@@ -4365,24 +4486,110 @@ var VioProductDetail = class extends LitElement {
4365
4486
  </div>`}
4366
4487
 
4367
4488
  <div class="info" part="info">
4368
- ${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
4369
- <div class="headline">
4370
- <div class="title-row">
4371
- <h2 class="name" part="title">${p.title}</h2>
4372
- <div class="stock ${stock.cls}" part="stock">${stock.text}</div>
4373
- </div>
4374
- <div class="price-row">
4489
+ ${this.renderSections(this.layoutNow.sections)}
4490
+ </div>
4491
+ </div>
4492
+ `;
4493
+ }
4494
+ /**
4495
+ * The info column in the applied layout's order (0.34) — the real DOM in
4496
+ * that order, never CSS `order`, the same on every screen. The default
4497
+ * order renders exactly the column before 0.34: brand, `.headline` (the
4498
+ * title row with the stock, the price row), then `.purchase` — `.buy` with
4499
+ * the options first — and highlights, description, «Frakt & retur» in it.
4500
+ *
4501
+ * A run of `title` / `stock` / `price` is one `.headline`; `title`
4502
+ * right before `stock` share the title row. `options` right before
4503
+ * `purchase` live in `.buy` (a pinned buy zone keeps them); elsewhere they
4504
+ * are a block of their own. Everything after `purchase` goes in
4505
+ * `.purchase`, as the description does by default: a pinned buy zone may
4506
+ * slide over it, never over what comes before.
4507
+ */
4508
+ renderSections(sections) {
4509
+ const out = [];
4510
+ for (let i = 0; i < sections.length; i++) {
4511
+ const s = sections[i];
4512
+ if (s === "title" || s === "stock" || s === "price") {
4513
+ let j = i;
4514
+ while (j < sections.length && ["title", "stock", "price"].includes(sections[j])) j++;
4515
+ out.push(this.renderHeadline(sections.slice(i, j)));
4516
+ i = j - 1;
4517
+ continue;
4518
+ }
4519
+ if (s === "options" && sections[i + 1] === "purchase") continue;
4520
+ if (s === "purchase") {
4521
+ const optionsInBuy = sections[i - 1] === "options";
4522
+ out.push(this.renderPurchase(optionsInBuy, sections.slice(i + 1)));
4523
+ break;
4524
+ }
4525
+ out.push(this.renderSection(s));
4526
+ }
4527
+ return out;
4528
+ }
4529
+ /** A run of title / stock / price: one `.headline`. */
4530
+ renderHeadline(run) {
4531
+ const p = this.product;
4532
+ const stock = this.stockLabel();
4533
+ const compare = this.compareAt;
4534
+ const showCompare = compare !== null && compare > this.unitPrice;
4535
+ const name = html`<h2 class="name" part="title">${p.title}</h2>`;
4536
+ const stockLine = html`<div class="stock ${stock.cls}" part="stock">${stock.text}</div>`;
4537
+ const parts = [];
4538
+ for (let i = 0; i < run.length; i++) {
4539
+ const s = run[i];
4540
+ if (s === "title" && run[i + 1] === "stock") {
4541
+ parts.push(html`<div class="title-row">
4542
+ ${name}
4543
+ ${stockLine}
4544
+ </div>`);
4545
+ i++;
4546
+ } else if (s === "title") {
4547
+ parts.push(html`<div class="title-row">${name}</div>`);
4548
+ } else if (s === "stock") {
4549
+ parts.push(stockLine);
4550
+ } else {
4551
+ parts.push(html`<div class="price-row">
4375
4552
  <span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
4376
4553
  ${showCompare ? html`<span class="compare-at">${formatPrice(
4377
- compare,
4378
- p.price.currency_code
4379
- )}</span>` : ""}
4380
- </div>
4381
- </div>
4382
-
4383
- <div class="purchase">
4554
+ compare,
4555
+ p.price.currency_code
4556
+ )}</span>` : ""}
4557
+ </div>`);
4558
+ }
4559
+ }
4560
+ return html`<div class="headline">
4561
+ ${parts}
4562
+ </div>`;
4563
+ }
4564
+ /** One section that is not the headline nor the purchase group. */
4565
+ renderSection(section) {
4566
+ const p = this.product;
4567
+ switch (section) {
4568
+ case "brand":
4569
+ return p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : "";
4570
+ case "options":
4571
+ return p.options.length > 0 ? html`<div class="buy-options options-apart" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : "";
4572
+ case "highlights":
4573
+ return this.renderHighlights();
4574
+ case "description":
4575
+ return p.description ? this.renderDescription(this.stripHtml(p.description)) : "";
4576
+ case "shippingReturns":
4577
+ return this.renderShippingReturns();
4578
+ default:
4579
+ return "";
4580
+ }
4581
+ }
4582
+ /**
4583
+ * The SDK-managed buy group, never split: «Frakt fra…», Vipps / Apple Pay /
4584
+ * the other ways to pay, quantity + «Legg i handlekurv», «Du handler direkte
4585
+ * hos …». The options when they come right before it; then what follows it.
4586
+ */
4587
+ renderPurchase(optionsInBuy, rest) {
4588
+ const p = this.product;
4589
+ const optionsApart = !optionsInBuy && p.options.length > 0;
4590
+ return html`<div class=${optionsApart ? "purchase options-apart" : "purchase"}>
4384
4591
  <div class="buy" part="buy">
4385
- ${p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
4592
+ ${optionsInBuy && p.options.length > 0 ? html`<div class="buy-options" part="buy-options">${this.renderOptionPickers(p.options)}</div>` : ""}
4386
4593
  ${this.renderDeliveryNote()}
4387
4594
  ${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
4388
4595
  ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
@@ -4411,7 +4618,7 @@ var VioProductDetail = class extends LitElement {
4411
4618
  <span class="vipps-badge">vipps</span>
4412
4619
  </button>
4413
4620
  ` : ""}
4414
- ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") && this.vippsGoesStraightToApp() ? html`<p class="vipps-note" part="vipps-note">Vipps fyller inn navn og adresse – du godkjenner og betaler i appen</p>` : ""}
4621
+ ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.renderVippsNote() : ""}
4415
4622
 
4416
4623
  <div class="qty-cta-row" part="quantity-row">
4417
4624
  <div class="qty-stepper">
@@ -4499,14 +4706,8 @@ var VioProductDetail = class extends LitElement {
4499
4706
  ${this.renderSellerNote()}
4500
4707
  </div>
4501
4708
 
4502
- ${this.renderHighlights()}
4503
-
4504
- ${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
4505
- ${this.renderShippingReturns()}
4506
- </div>
4507
- </div>
4508
- </div>
4509
- `;
4709
+ ${this.renderSections(rest)}
4710
+ </div>`;
4510
4711
  }
4511
4712
  renderOptionPickers(options) {
4512
4713
  if (options.length === 0) return "";
@@ -4590,6 +4791,9 @@ __decorateClass([
4590
4791
  __decorateClass([
4591
4792
  property({ type: Boolean, attribute: "no-added-toast" })
4592
4793
  ], VioProductDetail.prototype, "noAddedToast", 2);
4794
+ __decorateClass([
4795
+ property({ type: String, attribute: "vipps-cart-payment" })
4796
+ ], VioProductDetail.prototype, "vippsCartPayment", 2);
4593
4797
  __decorateClass([
4594
4798
  property({ type: String })
4595
4799
  ], VioProductDetail.prototype, "gallery", 2);
@@ -4605,6 +4809,9 @@ __decorateClass([
4605
4809
  __decorateClass([
4606
4810
  property({ type: Object, attribute: "carrier-logos" })
4607
4811
  ], VioProductDetail.prototype, "carrierLogos", 2);
4812
+ __decorateClass([
4813
+ property({ attribute: "layout", converter: layoutAttribute })
4814
+ ], VioProductDetail.prototype, "layout", 2);
4608
4815
  __decorateClass([
4609
4816
  state()
4610
4817
  ], VioProductDetail.prototype, "product", 2);
@@ -4731,12 +4938,15 @@ var VioCart = class extends LitElement {
4731
4938
  this.heading = "Handlekurv";
4732
4939
  this.emptyLabel = "Handlekurven er tom";
4733
4940
  this.subtotalLabel = "Sum";
4941
+ this.vippsCartPayment = "";
4734
4942
  this.carts = /* @__PURE__ */ new Map();
4735
4943
  this.itemCount = 0;
4736
4944
  this.applePayAvailable = false;
4737
4945
  this.availableMethods = null;
4738
4946
  this.methodsLoading = false;
4739
4947
  this.vippsOfficial = false;
4948
+ /** A Vipps payment is being started from here: one at a time. */
4949
+ this.vippsStarting = false;
4740
4950
  /** Whether the method lookup has been attempted — see boundOnCartChange. */
4741
4951
  this.paymentMethodsAttempted = false;
4742
4952
  this.confirmedOrder = null;
@@ -5312,7 +5522,6 @@ var VioCart = class extends LitElement {
5312
5522
  })
5313
5523
  );
5314
5524
  }
5315
- /** Vipps — opens the checkout with Vipps preselected. */
5316
5525
  /**
5317
5526
  * Open the checkout without preselecting a method.
5318
5527
  *
@@ -5334,21 +5543,73 @@ var VioCart = class extends LitElement {
5334
5543
  );
5335
5544
  }
5336
5545
  /**
5337
- * Vipps from the cart: the checkout opens with Vipps preselected — the
5338
- * standard path, where our form collects name, address, mobile and the
5339
- * delivery (with its price) before paying (Mote & Livsstil, 2026-10-28).
5340
- * It no longer starts Express from here: Express is the product detail's
5341
- * «Kjøp nå med Vipps» only.
5546
+ * Vipps from the cart (2026-10-29: «el botón negro va con el nuestro y el
5547
+ * botón de Vipps paga el carrito entero»): Vipps Express for the sponsor's
5548
+ * WHOLE cart — the same Express as the detail's «Kjøp nå med Vipps»: the
5549
+ * hand-over overlay, `vio:payment-redirect-*` with source `'cart'`, the
5550
+ * return and its confirmation. «Til kassen» is the way into our checkout.
5551
+ * When the brand has Express off, or this cart cannot have it (several
5552
+ * suppliers, no rate), the checkout opens with Vipps preselected.
5553
+ *
5554
+ * `vipps-cart-payment="checkout"` keeps the 0.31–0.34 behaviour: the
5555
+ * checkout opens with Vipps preselected, the standard payment.
5342
5556
  */
5343
5557
  onVipps() {
5344
5558
  if (!this.payable) return;
5345
- const firstSponsorId = [...this.carts.keys()][0];
5346
- if (firstSponsorId === void 0) return;
5559
+ if (resolveVippsCartPayment(this, this.vippsCartPayment) === "checkout") {
5560
+ const firstSponsorId = [...this.carts.keys()][0];
5561
+ if (firstSponsorId === void 0) return;
5562
+ this.openCheckoutWithVipps(firstSponsorId);
5563
+ return;
5564
+ }
5565
+ if (this.vippsStarting) return;
5566
+ const sponsorId = this.vippsSponsorId();
5567
+ if (sponsorId === void 0) return;
5568
+ this.vippsStarting = true;
5569
+ const redirect = beginPaymentRedirect({ method: "vipps", sponsorId, source: "cart" });
5570
+ void this.startVippsExpressOrCheckout(sponsorId, redirect).finally(() => {
5571
+ this.vippsStarting = false;
5572
+ });
5573
+ }
5574
+ /**
5575
+ * The one sponsor whose cart the Vipps button pays: the first with
5576
+ * something to pay for. Express is built on THAT sponsor's backend cart
5577
+ * only — another sponsor's lines are never in it.
5578
+ */
5579
+ vippsSponsorId() {
5580
+ for (const cart of this.carts.values()) {
5581
+ const total = cart.items.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
5582
+ if (cart.items.length > 0 && total > 0) return cart.sponsorId;
5583
+ }
5584
+ return void 0;
5585
+ }
5586
+ async startVippsExpressOrCheckout(sponsorId, redirect) {
5587
+ let fallback = true;
5588
+ try {
5589
+ await Vio.cart.waitForCartMutations(sponsorId);
5590
+ if (!await Vio.checkout.getVippsExpressEnabled(sponsorId)) {
5591
+ redirect.abort("express-off");
5592
+ } else {
5593
+ const res = await Vio.checkout.startVippsExpress(sponsorId);
5594
+ fallback = !res?.payment_url;
5595
+ if (fallback) redirect.abort("not-started");
5596
+ }
5597
+ } catch (err) {
5598
+ if (typeof console !== "undefined") {
5599
+ console.warn("[VioCart] Vipps Express not started:", err);
5600
+ }
5601
+ redirect.fail(err instanceof Error ? err.message : String(err));
5602
+ }
5603
+ if (!fallback) return;
5604
+ this.openCheckoutWithVipps(sponsorId);
5605
+ }
5606
+ /** Our checkout with Vipps preselected: the host opens it on `vio:checkout-open`. */
5607
+ openCheckoutWithVipps(sponsorId) {
5347
5608
  this.dispatchEvent(
5348
5609
  new CustomEvent("vio:checkout-open", {
5349
5610
  bubbles: true,
5350
5611
  composed: true,
5351
- detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(firstSponsorId) }
5612
+ detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(sponsorId) }
5352
5613
  })
5353
5614
  );
5354
5615
  }
@@ -5616,6 +5877,9 @@ __decorateClass([
5616
5877
  __decorateClass([
5617
5878
  property({ type: String, attribute: "subtotal-label" })
5618
5879
  ], VioCart.prototype, "subtotalLabel", 2);
5880
+ __decorateClass([
5881
+ property({ type: String, attribute: "vipps-cart-payment" })
5882
+ ], VioCart.prototype, "vippsCartPayment", 2);
5619
5883
  __decorateClass([
5620
5884
  state()
5621
5885
  ], VioCart.prototype, "carts", 2);
@@ -9391,5 +9655,5 @@ function registerVioElements() {
9391
9655
  }
9392
9656
 
9393
9657
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
9394
- //# sourceMappingURL=chunk-T5B74YBQ.js.map
9395
- //# sourceMappingURL=chunk-T5B74YBQ.js.map
9658
+ //# sourceMappingURL=chunk-F7LE5AZY.js.map
9659
+ //# sourceMappingURL=chunk-F7LE5AZY.js.map