@vio-live/web-sdk 0.32.0 → 0.33.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,54 @@ 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.33.0 — 2026-10-28
18
+
19
+ ### Fixed
20
+ - **One cart per connection.** Every connection of a site (environment + key) shared one stored
21
+ cart, and every tab adopted what the others wrote: two tabs on two connections (Staging and Prod
22
+ side by side) could take each other's cart for a moment. Each connection now keeps its own
23
+ (`vio.cart.v1:<environment>:<hash of the key>` — never the key itself), a tab only listens to
24
+ its own, and `Vio.init` with another key switches `Vio.cart` to that connection's carts (the
25
+ first one's come back if the page switches back). An answer that arrives for the connection the
26
+ page left lands in that connection's cart, silently — never in the new one.
27
+ - **The commerce key is the channel of the article's campaign, not the sponsor's first block.**
28
+ With two campaigns of one sponsor on a surface (Careless: campaign 2 on channel 482, an internal
29
+ test campaign 1 on channel 480), products, carts and checkouts used the default campaign's
30
+ channel even for an article of the other. The key now follows the campaign in context (the
31
+ click's `campaignId`, `vio-campaign`, `data-vio-campaign`, `Vio.init`'s), among the sponsor's
32
+ blocks (a block with `commerce: null` is skipped instead of throwing). `Vio.commerceFor(sponsorId)`
33
+ still works and takes an optional `{ campaignId?, channelId? }`; commerce clients are kept per
34
+ sponsor and key. New: `Vio.sponsorCommerce(sponsorId, ctx?)`.
35
+ - **A cart remembers its channel.** `SponsorCartState.channelId` is stored when the backend cart is
36
+ created, and every later cart and checkout call for it (also the return from Vipps / Stripe,
37
+ through the per-checkout memory) uses that channel's key. If the bootstrap no longer lists the
38
+ channel, the stale `cartId` is dropped and the cart is created again on the current channel with
39
+ its lines — never a call with another channel's key. Carts from before keep the sponsor's first
40
+ block, as when they were created.
41
+ - **The order's campaign belongs to the cart's sponsor.** `attribution.campaign_id` on the checkout
42
+ and `campaignId` on `vio:checkout-created` (→ `add_payment_info`) took the page's campaign even
43
+ when it was another sponsor's. Now: the campaign the cart's lines were added from, else the
44
+ page's (`data-vio-campaign` / `vio-campaign` on the page, then `Vio.init`'s) only when the sponsor
45
+ takes part in it, else the slices' rule — only campaigns of that sponsor.
46
+ - **The bootstrap no longer lives for the whole page.** When the tab becomes visible again and the
47
+ cached bootstrap is older than 5 minutes, it is fetched again (the old one stays until the new
48
+ one arrives, and if the refresh fails). Carts keep their lines; an open checkout keeps paying
49
+ with its channel's key.
50
+ - **«Frakt & retur» no longer shows filler as the delivery time.** A rate's description equal to
51
+ its own name (dashboard-created rates copied it), `By Outshifter` or `Default Shipping Method`
52
+ (products' defaults) is no description in `getShippingRates`; the detail also drops one equal
53
+ to the product's title or the brand / seller name.
54
+
55
+ ### ⚠️ Upgrade notes
56
+ - **Hosts with several articles (Replit, Vev):** pass `sponsorId` **and** `campaignId` on
57
+ `vio:product-click`, set `data-vio-campaign` on each article's wrapper, and do **not** set a
58
+ site-wide `Vio.init({ campaignId })` — the campaign in context decides which channel sells.
59
+ - The carts kept in the old shared entry move, once, to the first connection that opens with none
60
+ of its own. Nothing to do.
61
+ - `Vio.cart` is the same object for the whole page, so listeners added to it keep working across a
62
+ re-init. A host that archived and restored carts per connection itself (Replit's
63
+ `vioCartConnections`) can rely on this instead.
64
+
17
65
  ## 0.32.0 — 2026-10-28
18
66
 
19
67
  > First version published to npm since 0.29.0: **0.30.0 and 0.31.0 were never published on their own** — their
package/README.md CHANGED
@@ -226,7 +226,7 @@ a campaign to a slot. But an editorial page — a Vev article, a Replit article
226
226
  often *is* part of one campaign, and the page can say so:
227
227
 
228
228
  ```ts
229
- Vio.init({ apiKey: '…', campaignId: 12 }) // the whole page
229
+ Vio.init({ apiKey: '…', campaignId: 12 }) // the whole page — only on a page of ONE campaign
230
230
  ```
231
231
 
232
232
  ```html
@@ -243,6 +243,19 @@ ancestors. On web/Replit a **per-article** campaign needs a per-article scope to
243
243
  same `carousel-1`, and the articles' campaigns would overwrite one another's
244
244
  assignment; Vev ids are per block, so it does not apply there. What it does:
245
245
 
246
+ - **Which channel sells.** One article = one campaign = one channel = one
247
+ sponsor, and one sponsor may run several campaigns on a surface, each on its own
248
+ commerce channel. The SDK reads products, creates the cart and pays with the key
249
+ of the channel of the **campaign in context** — the click's `campaignId`, the
250
+ element's `vio-campaign`, an ancestor's `data-vio-campaign`, then `Vio.init`'s —
251
+ among the blocks of that sponsor (`Vio.sponsorCommerce(sponsorId, { campaignId })`,
252
+ `Vio.commerceFor(sponsorId, { campaignId })`); without one, the sponsor's first
253
+ block, as before. A cart remembers the channel it was created on (`channelId`)
254
+ and every later cart and checkout call uses that channel's key; if the bootstrap
255
+ no longer lists that channel, the cart is created again on the current one with
256
+ its lines. So on a site with several articles (Replit, Vev), **set
257
+ `data-vio-campaign` per article and pass `sponsorId` and `campaignId` on
258
+ `vio:product-click`; do not set a site-wide `Vio.init({ campaignId })`.**
246
259
  - **Declaration.** The manifest carries the page's campaign (top-level) and an
247
260
  element's own campaign per location when it differs. The campaigns service
248
261
  (campaigns micro PR #16) turns that into assignments of the page's slots
@@ -372,9 +385,11 @@ Since 0.26 the origins do not stay in the browser: **Commerce receives them**
372
385
  - **Per checkout.** `CreateCheckout` and the `UpdateCheckout` right before paying
373
386
  carry `attribution`: `surface_key` (the SDK key — the gateway sets it from
374
387
  `X-API-Key` anyway, and shopcart checks it against the cart's channel, so a page
375
- cannot credit a sale to another surface), `campaign_id` (the page's campaign,
376
- `Vio.pageCampaign()` — else the campaign that explains most units of the cart under
377
- the same rule `purchase` is split by), `content_url` / `content_title` (the page at
388
+ cannot credit a sale to another surface), `campaign_id` (always a campaign the
389
+ cart's sponsor takes part in: the campaign the cart's lines were added from, else
390
+ the page's — `data-vio-campaign` / `vio-campaign` on the page, then `Vio.init`'s —
391
+ else the campaign that explains most units of the cart under the same rule
392
+ `purchase` is split by), `content_url` / `content_title` (the page at
378
393
  pay time) and `entry` (`{ referrer, utm_source, utm_medium, utm_campaign,
379
394
  landing_url }`: the landing the session **remembers** — the same memory the
380
395
  `client` block reads, which now keeps the landing page too — never the page the
@@ -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, 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';
2
2
  import { LitElement, css, html, nothing } from 'lit';
3
3
  import { property, state, query } from 'lit/decorators.js';
4
4
 
@@ -1151,6 +1151,21 @@ var VioProductCarousel = class extends LitElement {
1151
1151
  * sponsor of the campaign the slot shows (ADR-0034 — "the campaign of the
1152
1152
  * block is its sponsor"), else 0 (unknown).
1153
1153
  */
1154
+ /**
1155
+ * The campaign whose channel sells a sponsor's products here (one article =
1156
+ * one campaign = one channel): the campaign this slot credits for that
1157
+ * sponsor (`Vio.attribution`), then the element's own (`vio-campaign`, an
1158
+ * ancestor's `data-vio-campaign`).
1159
+ */
1160
+ commerceCampaigns(sponsorId) {
1161
+ const own = this.slotRegistration.campaignHint;
1162
+ try {
1163
+ const resolved = typeof Vio.attribution === "function" ? Vio.attribution(this.effectiveSlot || null, { sponsorId, campaignId: own }).campaignId : void 0;
1164
+ return resolved !== void 0 ? [resolved, ...own] : own;
1165
+ } catch {
1166
+ return own;
1167
+ }
1168
+ }
1154
1169
  effectiveSponsorId() {
1155
1170
  if (this.sponsorId > 0) return this.sponsorId;
1156
1171
  const fromSlot = typeof Vio.campaignForSlot === "function" ? Number(Vio.campaignForSlot(this.effectiveSlot || null, { campaignId: this.slotRegistration.campaignHint })?.primarySponsor?.id) : NaN;
@@ -1188,7 +1203,7 @@ var VioProductCarousel = class extends LitElement {
1188
1203
  }
1189
1204
  const fetched = await Promise.all(
1190
1205
  Array.from(bySponsor.entries()).map(async ([sponsorId2, ids]) => {
1191
- const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2);
1206
+ const commerce = this.commerceKey && this.productRefs.trim() === "" ? Vio.commerceWithKey(this.commerceKey) : Vio.commerceFor(sponsorId2, { campaignId: this.commerceCampaigns(sponsorId2) });
1192
1207
  const products = await commerce.channel.product.getByIds({
1193
1208
  product_ids: ids,
1194
1209
  currency: this.currency,
@@ -2204,6 +2219,21 @@ var VioProductDetail = class extends LitElement {
2204
2219
  const own = this.slotRegistration.campaignHint;
2205
2220
  return this.fromCard && this.eventCampaignId !== void 0 ? [this.eventCampaignId, ...own] : own;
2206
2221
  }
2222
+ /**
2223
+ * The campaign whose channel sells the open product (one article = one
2224
+ * campaign = one channel): the campaign its events credit, then the opening
2225
+ * click's and this element's own — so the product, its rates and its
2226
+ * seller are read with that channel's key, the one the cart is created on.
2227
+ */
2228
+ commerceCampaigns() {
2229
+ const hint = this.activeCampaignHint;
2230
+ try {
2231
+ const resolved = this.attribution().campaignId;
2232
+ return typeof resolved === "number" ? [resolved, ...hint] : hint;
2233
+ } catch {
2234
+ return hint;
2235
+ }
2236
+ }
2207
2237
  slotAttributes() {
2208
2238
  return {
2209
2239
  vioSlot: this.vioSlot,
@@ -3319,7 +3349,7 @@ var VioProductDetail = class extends LitElement {
3319
3349
  try {
3320
3350
  const activeCurrency = getGlobalCurrency() || this.currency;
3321
3351
  this.currency = activeCurrency;
3322
- const commerce = Vio.commerceFor(this.sponsorId);
3352
+ const commerce = Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() });
3323
3353
  products = await commerce.channel.product.getByIds({
3324
3354
  product_ids: [productIdNum],
3325
3355
  currency: activeCurrency,
@@ -4019,7 +4049,7 @@ var VioProductDetail = class extends LitElement {
4019
4049
  async loadShippingFrom(productId, seq) {
4020
4050
  let rates = null;
4021
4051
  try {
4022
- const product = Vio.commerceFor(this.sponsorId).channel.product;
4052
+ const product = Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() }).channel.product;
4023
4053
  rates = typeof product.getShippingRates === "function" ? await product.getShippingRates(productId, { currency: this.currency }) : null;
4024
4054
  } catch {
4025
4055
  rates = null;
@@ -4135,7 +4165,12 @@ var VioProductDetail = class extends LitElement {
4135
4165
  */
4136
4166
  renderShippingReturns() {
4137
4167
  const identity = resolveSponsorIdentity(this.sponsorId);
4138
- const rates = this.shippingRates;
4168
+ const rates = shippingRatesShown(this.shippingRates, [
4169
+ this.product?.title,
4170
+ this.product?.brand,
4171
+ this.sellerName,
4172
+ identity?.name
4173
+ ]);
4139
4174
  const times = [...new Set(rates.map((r) => r.description).filter(Boolean))];
4140
4175
  const perRateTime = times.length > 1;
4141
4176
  const sharedTime = times.length === 1 ? times[0] : times.length === 0 && this.deliveryTime.trim() ? `Leveringstid: ${this.deliveryTime.trim()}` : "";
@@ -4188,7 +4223,7 @@ var VioProductDetail = class extends LitElement {
4188
4223
  async loadSeller(productId, seq) {
4189
4224
  let name = null;
4190
4225
  try {
4191
- name = await Vio.commerceFor(this.sponsorId).channel.product.getSellerCompany(productId);
4226
+ name = await Vio.commerceFor(this.sponsorId, { campaignId: this.commerceCampaigns() }).channel.product.getSellerCompany(productId);
4192
4227
  } catch {
4193
4228
  name = null;
4194
4229
  }
@@ -4657,6 +4692,9 @@ function normalizeValues(raw) {
4657
4692
  }
4658
4693
  return [];
4659
4694
  }
4695
+ function shippingRatesShown(rates, repeats = []) {
4696
+ return rates.map((r) => ({ ...r, description: shippingDescriptionOf(r.description, [r.name, ...repeats]) }));
4697
+ }
4660
4698
  function splitRateName(name) {
4661
4699
  const text = String(name ?? "").trim();
4662
4700
  const m = text.match(/^(.+?)\s+[-–—]\s+(.+)$/);
@@ -9353,5 +9391,5 @@ function registerVioElements() {
9353
9391
  }
9354
9392
 
9355
9393
  export { VioCart, VioCheckout, VioProduct, VioProductCarousel, VioProductDetail, VioTokens, applyVioTheme, registerVioElements };
9356
- //# sourceMappingURL=chunk-C5Q6W4B2.js.map
9357
- //# sourceMappingURL=chunk-C5Q6W4B2.js.map
9394
+ //# sourceMappingURL=chunk-T5B74YBQ.js.map
9395
+ //# sourceMappingURL=chunk-T5B74YBQ.js.map