@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.
@@ -1,4 +1,4 @@
1
- import { Vio, __decorateClass, primaryImageUrl, displayPrice, toCampaignId, getGlobalCurrency, ensureVippsButton, formatPrice, 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-IR4XYTZL.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;
@@ -1151,6 +1152,21 @@ var VioProductCarousel = class extends LitElement {
1151
1152
  * sponsor of the campaign the slot shows (ADR-0034 — "the campaign of the
1152
1153
  * block is its sponsor"), else 0 (unknown).
1153
1154
  */
1155
+ /**
1156
+ * The campaign whose channel sells a sponsor's products here (one article =
1157
+ * one campaign = one channel): the campaign this slot credits for that
1158
+ * sponsor (`Vio.attribution`), then the element's own (`vio-campaign`, an
1159
+ * ancestor's `data-vio-campaign`).
1160
+ */
1161
+ commerceCampaigns(sponsorId) {
1162
+ const own = this.slotRegistration.campaignHint;
1163
+ try {
1164
+ const resolved = typeof Vio.attribution === "function" ? Vio.attribution(this.effectiveSlot || null, { sponsorId, campaignId: own }).campaignId : void 0;
1165
+ return resolved !== void 0 ? [resolved, ...own] : own;
1166
+ } catch {
1167
+ return own;
1168
+ }
1169
+ }
1154
1170
  effectiveSponsorId() {
1155
1171
  if (this.sponsorId > 0) return this.sponsorId;
1156
1172
  const fromSlot = typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.effectiveSlot || null, { campaignId: this.slotRegistration.campaignHint })?.primarySponsor?.id) : NaN;
@@ -1188,7 +1204,7 @@ var VioProductCarousel = class extends LitElement {
1188
1204
  }
1189
1205
  const fetched = await Promise.all(
1190
1206
  Array.from(bySponsor.entries()).map(async ([sponsorId2, ids]) => {
1191
- const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2);
1207
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2, { campaignId: this.commerceCampaigns(sponsorId2) });
1192
1208
  const products = await commerce.channel.product.getByIds({
1193
1209
  product_ids: ids,
1194
1210
  currency: this.currency,
@@ -1541,6 +1557,21 @@ function returnCancelled(detail) {
1541
1557
  emit("vio:payment-redirect-cancel", { ...detail, reason: "cancelled" });
1542
1558
  }
1543
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
+
1544
1575
  // src/core/checkout/method-taxonomy.ts
1545
1576
  function normalizeMethodName(name) {
1546
1577
  return String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
@@ -2022,6 +2053,19 @@ function unlockPageScroll() {
2022
2053
  }
2023
2054
 
2024
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
+ };
2025
2069
  var ADDED_TOAST_MS = 8e3;
2026
2070
  var STRIPE_LOGO_SRC = "data:image/svg+xml;utf8," + encodeURIComponent(
2027
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>'
@@ -2072,11 +2116,18 @@ var VioProductDetail = class extends LitElement {
2072
2116
  this.vioContentId = "";
2073
2117
  this.vioContentTitle = "";
2074
2118
  this.noAddedToast = false;
2119
+ this.vippsCartPayment = "";
2075
2120
  this.gallery = "";
2076
2121
  this.descriptionMode = "";
2077
2122
  this.highlights = [];
2078
2123
  this.deliveryTime = "";
2079
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;
2080
2131
  this.slotRegistration = new SlotRegistration(this, "detail");
2081
2132
  /** What the click that opened the detail carried ('' when nothing), and for which product. */
2082
2133
  this.eventSlot = "";
@@ -2165,6 +2216,10 @@ var VioProductDetail = class extends LitElement {
2165
2216
  this.show();
2166
2217
  void this.fetchProduct();
2167
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
+ };
2168
2223
  /** The photo in view is the active one (dots, Apple Pay sheet, events). */
2169
2224
  this.onCarouselScroll = () => {
2170
2225
  const track = this.carouselTrack();
@@ -2183,6 +2238,14 @@ var VioProductDetail = class extends LitElement {
2183
2238
  if (atEnd !== this.thumbsAtEnd) this.thumbsAtEnd = atEnd;
2184
2239
  };
2185
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
+ }
2186
2249
  /** The card's context applies to the product it opened — not to one the host opened by hand later. */
2187
2250
  get fromCard() {
2188
2251
  return this.eventProductId !== "" && this.eventProductId === String(this.productId);
@@ -2204,6 +2267,21 @@ var VioProductDetail = class extends LitElement {
2204
2267
  const own = this.slotRegistration.campaignHint;
2205
2268
  return this.fromCard && this.eventCampaignId !== void 0 ? [this.eventCampaignId, ...own] : own;
2206
2269
  }
2270
+ /**
2271
+ * The campaign whose channel sells the open product (one article = one
2272
+ * campaign = one channel): the campaign its events credit, then the opening
2273
+ * click's and this element's own — so the product, its rates and its
2274
+ * seller are read with that channel's key, the one the cart is created on.
2275
+ */
2276
+ commerceCampaigns() {
2277
+ const hint = this.activeCampaignHint;
2278
+ try {
2279
+ const resolved = this.attribution().campaignId;
2280
+ return typeof resolved === "number" ? [resolved, ...hint] : hint;
2281
+ } catch {
2282
+ return hint;
2283
+ }
2284
+ }
2207
2285
  slotAttributes() {
2208
2286
  return {
2209
2287
  vioSlot: this.vioSlot,
@@ -2684,6 +2762,10 @@ var VioProductDetail = class extends LitElement {
2684
2762
  }
2685
2763
  /* The sizes follow the price closely: they are the next thing to do. */
2686
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; }
2687
2769
  .price {
2688
2770
  font-size: var(--vio-detail-price-size, 24px);
2689
2771
  font-weight: var(--vio-detail-price-weight, 700);
@@ -3175,6 +3257,9 @@ var VioProductDetail = class extends LitElement {
3175
3257
  }
3176
3258
  connectedCallback() {
3177
3259
  super.connectedCallback();
3260
+ Vio.cart.addEventListener("change", this.boundCartChange);
3261
+ this.unsubscribeLayout?.();
3262
+ this.unsubscribeLayout = onGlobalDetailLayoutChange(() => this.requestUpdate());
3178
3263
  this.slotRegistration.connect(this.slotAttributes());
3179
3264
  void this.loadVippsButton();
3180
3265
  document.addEventListener("vio:product-click", this.boundProductClick);
@@ -3183,8 +3268,11 @@ var VioProductDetail = class extends LitElement {
3183
3268
  if (this.addedToastTimer) clearTimeout(this.addedToastTimer);
3184
3269
  this.addedToastTimer = null;
3185
3270
  document.removeEventListener("vio:product-click", this.boundProductClick);
3271
+ Vio.cart.removeEventListener("change", this.boundCartChange);
3186
3272
  this.slotRegistration.clear();
3187
3273
  this.bottomSheet.disconnect();
3274
+ this.unsubscribeLayout?.();
3275
+ this.unsubscribeLayout = null;
3188
3276
  super.disconnectedCallback();
3189
3277
  }
3190
3278
  updated(changed) {
@@ -3208,17 +3296,38 @@ var VioProductDetail = class extends LitElement {
3208
3296
  else this.revealActiveSlide();
3209
3297
  }
3210
3298
  this.measureLogo();
3299
+ this.announceLayout();
3211
3300
  if (changed.has("vioSlot") || changed.has("vioSlotName") || changed.has("vioScope") || changed.has("vioScopeName") || changed.has("vioCampaign")) {
3212
3301
  this.slotRegistration.sync(this.slotAttributes());
3213
3302
  }
3214
3303
  }
3215
3304
  willUpdate(changed) {
3305
+ this.layoutNow = resolveDetailLayout(this.layout);
3216
3306
  if (changed.has("gallery") || changed.has("open") && this.open) this.galleryMode = this.resolveGalleryMode();
3217
3307
  if (changed.has("descriptionMode") || changed.has("open") && this.open) {
3218
3308
  const value = this.themeChoice(this.descriptionMode, "--vio-detail-description-mode");
3219
3309
  this.resolvedDescriptionMode = value === "folded" || value === "open" ? value : "preview";
3220
3310
  }
3221
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
+ }
3222
3331
  /** An option as the element says it, else as the theme's custom property says it (lower case, unquoted). */
3223
3332
  themeChoice(own, cssVar) {
3224
3333
  const fromTheme = typeof getComputedStyle === "function" ? getComputedStyle(this).getPropertyValue(cssVar) : "";
@@ -3319,7 +3428,7 @@ var VioProductDetail = class extends LitElement {
3319
3428
  try {
3320
3429
  const activeCurrency = getGlobalCurrency() || this.currency;
3321
3430
  this.currency = activeCurrency;
3322
- const commerce = Vio.commerceFor(this.sponsorId);
3431
+ const commerce = Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() });
3323
3432
  products = await commerce.channel.product.getByIds({
3324
3433
  product_ids: [productIdNum],
3325
3434
  currency: activeCurrency,
@@ -3348,10 +3457,17 @@ var VioProductDetail = class extends LitElement {
3348
3457
  const price = Number(
3349
3458
  this.product.price?.amount_incl_taxes ?? this.product.price?.amount
3350
3459
  );
3460
+ const shown = layoutOriginOf(resolveDetailLayout(this.layout));
3351
3461
  Vio.analytics.track("view_item", {
3352
3462
  context: {
3353
3463
  sponsorId: this.sponsorId > 0 ? Number(this.sponsorId) : void 0,
3354
- ...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
+ } : {}
3355
3471
  },
3356
3472
  commerce: {
3357
3473
  items: [
@@ -3682,7 +3798,8 @@ var VioProductDetail = class extends LitElement {
3682
3798
  this.selectionPrompt = missing.name;
3683
3799
  void this.updateComplete.then(() => {
3684
3800
  const group = this.renderRoot.querySelector(".option-group.needs-choice");
3685
- group?.scrollIntoView?.({ behavior: "smooth", block: "nearest" });
3801
+ const apart = !!group?.closest(".options-apart");
3802
+ group?.scrollIntoView?.({ behavior: "smooth", block: apart ? "center" : "nearest" });
3686
3803
  });
3687
3804
  return false;
3688
3805
  }
@@ -3752,13 +3869,14 @@ var VioProductDetail = class extends LitElement {
3752
3869
  const variant = this.selectedVariant();
3753
3870
  const sponsorName = resolveSponsorIdentity(this.sponsorId)?.name || void 0;
3754
3871
  const { campaignId, slot, contentId, componentTemplateId, contentUrl, contentTitle } = this.attribution();
3872
+ const layout = layoutOriginOf(this.layoutNow);
3755
3873
  Vio.cart.addProduct({
3756
3874
  product: this.product,
3757
3875
  sponsorId: this.sponsorId,
3758
3876
  sponsorName,
3759
3877
  quantity: this.quantity,
3760
3878
  variantId: variant?.id,
3761
- origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle }
3879
+ origin: { campaignId, slot, contentId, componentKind: componentTemplateId, contentUrl, contentTitle, ...layout }
3762
3880
  });
3763
3881
  return variant?.id;
3764
3882
  }
@@ -3777,7 +3895,9 @@ var VioProductDetail = class extends LitElement {
3777
3895
  sponsorId: this.sponsorId,
3778
3896
  quantity: this.quantity,
3779
3897
  variantId,
3780
- ...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) } : {}
3781
3901
  }
3782
3902
  })
3783
3903
  );
@@ -3890,7 +4010,7 @@ var VioProductDetail = class extends LitElement {
3890
4010
  if (!this.product || this.availableQuantity <= 0 || this.adding) return;
3891
4011
  if (!this.requireSelection()) return;
3892
4012
  if (!(this.unitPrice > 0)) return;
3893
- if ((Vio.cart.getCart(this.sponsorId)?.items.length ?? 0) > 0) {
4013
+ if (this.cartHasLines() && this.vippsCartMode() === "checkout") {
3894
4014
  this.addCurrentToCart();
3895
4015
  this.close();
3896
4016
  this.openCheckoutWithVipps();
@@ -3902,12 +4022,46 @@ var VioProductDetail = class extends LitElement {
3902
4022
  void this.startVippsExpressOrCheckout(redirect);
3903
4023
  }
3904
4024
  /**
3905
- * The Vipps button will open the app with Express (see buyWithVipps): the
3906
- * brand has Express on, and this brand's cart is empty — with lines in it
3907
- * 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.
3908
4028
  */
3909
4029
  vippsGoesStraightToApp() {
3910
- 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 "";
3911
4065
  }
3912
4066
  async startVippsExpressOrCheckout(redirect) {
3913
4067
  let fallback = true;
@@ -3982,8 +4136,8 @@ var VioProductDetail = class extends LitElement {
3982
4136
  * after an await, so it cannot wait for the server's cart), while the
3983
4137
  * charge is the sponsor's WHOLE cart. So the sheet opens only when it shows
3984
4138
  * what will be charged: an empty cart, and a sheet prepared for this
3985
- * product × quantity. Otherwise — lines already in the cart (as with Vipps,
3986
- * 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 —
3987
4141
  * the product is added and the checkout opens with Apple Pay preselected:
3988
4142
  * every line and the total on screen, and its own Apple Pay button pays
3989
4143
  * that total.
@@ -4019,7 +4173,7 @@ var VioProductDetail = class extends LitElement {
4019
4173
  async loadShippingFrom(productId, seq) {
4020
4174
  let rates = null;
4021
4175
  try {
4022
- const product = Vio.commerceFor(this.sponsorId).channel.product;
4176
+ const product = Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() }).channel.product;
4023
4177
  rates = typeof product.getShippingRates === "function" ? await product.getShippingRates(productId, { currency: this.currency }) : null;
4024
4178
  } catch {
4025
4179
  rates = null;
@@ -4135,7 +4289,12 @@ var VioProductDetail = class extends LitElement {
4135
4289
  */
4136
4290
  renderShippingReturns() {
4137
4291
  const identity = resolveSponsorIdentity(this.sponsorId);
4138
- const rates = this.shippingRates;
4292
+ const rates = shippingRatesShown(this.shippingRates, [
4293
+ this.product?.title,
4294
+ this.product?.brand,
4295
+ this.sellerName,
4296
+ identity?.name
4297
+ ]);
4139
4298
  const times = [...new Set(rates.map((r) => r.description).filter(Boolean))];
4140
4299
  const perRateTime = times.length > 1;
4141
4300
  const sharedTime = times.length === 1 ? times[0] : times.length === 0 && this.deliveryTime.trim() ? `Leveringstid: ${this.deliveryTime.trim()}` : "";
@@ -4188,7 +4347,7 @@ var VioProductDetail = class extends LitElement {
4188
4347
  async loadSeller(productId, seq) {
4189
4348
  let name = null;
4190
4349
  try {
4191
- name = await Vio.commerceFor(this.sponsorId).channel.product.getSellerCompany(productId);
4350
+ name = await Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() }).channel.product.getSellerCompany(productId);
4192
4351
  } catch {
4193
4352
  name = null;
4194
4353
  }
@@ -4283,9 +4442,6 @@ var VioProductDetail = class extends LitElement {
4283
4442
  }
4284
4443
  if (!this.product) return html``;
4285
4444
  const p = this.product;
4286
- const stock = this.stockLabel();
4287
- const compare = this.compareAt;
4288
- const showCompare = compare !== null && compare > this.unitPrice;
4289
4445
  const images = this.allImages();
4290
4446
  const activeImage = this.activeImage;
4291
4447
  return html`
@@ -4330,24 +4486,110 @@ var VioProductDetail = class extends LitElement {
4330
4486
  </div>`}
4331
4487
 
4332
4488
  <div class="info" part="info">
4333
- ${p.brand ? html`<div class="brand" part="brand">${p.brand}</div>` : ""}
4334
- <div class="headline">
4335
- <div class="title-row">
4336
- <h2 class="name" part="title">${p.title}</h2>
4337
- <div class="stock ${stock.cls}" part="stock">${stock.text}</div>
4338
- </div>
4339
- <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">
4340
4552
  <span class="price" part="price">${formatPrice(this.unitPrice, p.price.currency_code)}</span>
4341
4553
  ${showCompare ? html`<span class="compare-at">${formatPrice(
4342
- compare,
4343
- p.price.currency_code
4344
- )}</span>` : ""}
4345
- </div>
4346
- </div>
4347
-
4348
- <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"}>
4349
4591
  <div class="buy" part="buy">
4350
- ${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>` : ""}
4351
4593
  ${this.renderDeliveryNote()}
4352
4594
  ${this.methodsPending && this.availableQuantity > 0 ? html`<div class="buy-pending" part="buy-pending" role="status" aria-label="Laster betalingsmåter"></div>` : ""}
4353
4595
  ${this.availableQuantity > 0 && this.hasPrice && this.methodEnabled("vipps") ? this.vippsOfficial ? html`
@@ -4376,7 +4618,7 @@ var VioProductDetail = class extends LitElement {
4376
4618
  <span class="vipps-badge">vipps</span>
4377
4619
  </button>
4378
4620
  ` : ""}
4379
- ${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() : ""}
4380
4622
 
4381
4623
  <div class="qty-cta-row" part="quantity-row">
4382
4624
  <div class="qty-stepper">
@@ -4464,14 +4706,8 @@ var VioProductDetail = class extends LitElement {
4464
4706
  ${this.renderSellerNote()}
4465
4707
  </div>
4466
4708
 
4467
- ${this.renderHighlights()}
4468
-
4469
- ${p.description ? this.renderDescription(this.stripHtml(p.description)) : ""}
4470
- ${this.renderShippingReturns()}
4471
- </div>
4472
- </div>
4473
- </div>
4474
- `;
4709
+ ${this.renderSections(rest)}
4710
+ </div>`;
4475
4711
  }
4476
4712
  renderOptionPickers(options) {
4477
4713
  if (options.length === 0) return "";
@@ -4555,6 +4791,9 @@ __decorateClass([
4555
4791
  __decorateClass([
4556
4792
  property({ type: Boolean, attribute: "no-added-toast" })
4557
4793
  ], VioProductDetail.prototype, "noAddedToast", 2);
4794
+ __decorateClass([
4795
+ property({ type: String, attribute: "vipps-cart-payment" })
4796
+ ], VioProductDetail.prototype, "vippsCartPayment", 2);
4558
4797
  __decorateClass([
4559
4798
  property({ type: String })
4560
4799
  ], VioProductDetail.prototype, "gallery", 2);
@@ -4570,6 +4809,9 @@ __decorateClass([
4570
4809
  __decorateClass([
4571
4810
  property({ type: Object, attribute: "carrier-logos" })
4572
4811
  ], VioProductDetail.prototype, "carrierLogos", 2);
4812
+ __decorateClass([
4813
+ property({ attribute: "layout", converter: layoutAttribute })
4814
+ ], VioProductDetail.prototype, "layout", 2);
4573
4815
  __decorateClass([
4574
4816
  state()
4575
4817
  ], VioProductDetail.prototype, "product", 2);
@@ -4657,6 +4899,9 @@ function normalizeValues(raw) {
4657
4899
  }
4658
4900
  return [];
4659
4901
  }
4902
+ function shippingRatesShown(rates, repeats = []) {
4903
+ return rates.map((r) => ({ ...r, description: shippingDescriptionOf(r.description, [r.name, ...repeats]) }));
4904
+ }
4660
4905
  function splitRateName(name) {
4661
4906
  const text = String(name ?? "").trim();
4662
4907
  const m = text.match(/^(.+?)\s+[-–—]\s+(.+)$/);
@@ -4693,12 +4938,15 @@ var VioCart = class extends LitElement {
4693
4938
  this.heading = "Handlekurv";
4694
4939
  this.emptyLabel = "Handlekurven er tom";
4695
4940
  this.subtotalLabel = "Sum";
4941
+ this.vippsCartPayment = "";
4696
4942
  this.carts = /* @__PURE__ */ new Map();
4697
4943
  this.itemCount = 0;
4698
4944
  this.applePayAvailable = false;
4699
4945
  this.availableMethods = null;
4700
4946
  this.methodsLoading = false;
4701
4947
  this.vippsOfficial = false;
4948
+ /** A Vipps payment is being started from here: one at a time. */
4949
+ this.vippsStarting = false;
4702
4950
  /** Whether the method lookup has been attempted — see boundOnCartChange. */
4703
4951
  this.paymentMethodsAttempted = false;
4704
4952
  this.confirmedOrder = null;
@@ -5274,7 +5522,6 @@ var VioCart = class extends LitElement {
5274
5522
  })
5275
5523
  );
5276
5524
  }
5277
- /** Vipps — opens the checkout with Vipps preselected. */
5278
5525
  /**
5279
5526
  * Open the checkout without preselecting a method.
5280
5527
  *
@@ -5296,21 +5543,73 @@ var VioCart = class extends LitElement {
5296
5543
  );
5297
5544
  }
5298
5545
  /**
5299
- * Vipps from the cart: the checkout opens with Vipps preselected — the
5300
- * standard path, where our form collects name, address, mobile and the
5301
- * delivery (with its price) before paying (Mote & Livsstil, 2026-10-28).
5302
- * It no longer starts Express from here: Express is the product detail's
5303
- * «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.
5304
5556
  */
5305
5557
  onVipps() {
5306
5558
  if (!this.payable) return;
5307
- const firstSponsorId = [...this.carts.keys()][0];
5308
- 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) {
5309
5608
  this.dispatchEvent(
5310
5609
  new CustomEvent("vio:checkout-open", {
5311
5610
  bubbles: true,
5312
5611
  composed: true,
5313
- detail: { sponsorId: firstSponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(firstSponsorId) }
5612
+ detail: { sponsorId, paymentMethod: "vipps", express: false, ...sponsorDetail(sponsorId) }
5314
5613
  })
5315
5614
  );
5316
5615
  }
@@ -5578,6 +5877,9 @@ __decorateClass([
5578
5877
  __decorateClass([
5579
5878
  property({ type: String, attribute: "subtotal-label" })
5580
5879
  ], VioCart.prototype, "subtotalLabel", 2);
5880
+ __decorateClass([
5881
+ property({ type: String, attribute: "vipps-cart-payment" })
5882
+ ], VioCart.prototype, "vippsCartPayment", 2);
5581
5883
  __decorateClass([
5582
5884
  state()
5583
5885
  ], VioCart.prototype, "carts", 2);
@@ -9353,5 +9655,5 @@ function registerVioElements() {
9353
9655
  }
9354
9656
 
9355
9657
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
9356
- //# sourceMappingURL=chunk-C5Q6W4B2.js.map
9357
- //# sourceMappingURL=chunk-C5Q6W4B2.js.map
9658
+ //# sourceMappingURL=chunk-F7LE5AZY.js.map
9659
+ //# sourceMappingURL=chunk-F7LE5AZY.js.map