@lime-bundles/widget 4.5.1 → 4.6.1

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/README.md CHANGED
@@ -62,9 +62,12 @@ document.querySelector("lime-bundle").addEventListener(
62
62
  | `app-url` | | Enables impression + add-to-cart analytics when set. |
63
63
  | `analytics` | | `"false"` suppresses analytics even when `app-url` is set. |
64
64
  | `locale` | | BCP-47 tag forwarded to the Storefront API. |
65
- | `country` | | ISO-3166 alpha-2 (e.g. `"US"`). Adds `@inContext(country:)` to Storefront queries — Markets pricing + currency. |
65
+ | `country` | | ISO-3166 alpha-2 (e.g. `"US"`). Adds `@inContext(country:)` to Storefront queries — Markets pricing + currency — and matches market-restricted bundles' projected country codes. |
66
66
  | `language` | | ISO-639-1 (e.g. `"EN"`). Adds `@inContext(language:)`. |
67
- | `market-id` | | Current visitor's Market GID. Hides market-restricted bundles whose allow-list doesn't include this market. |
67
+ | `market-id` | | Current visitor's Market GID. Legacy market-gate signal; prefer `country` (retail) and the `buyer` property (B2B) — a B2B buyer's company-location market has no storefront market context. |
68
+ | `currency-rate` | | Store→presentment currency rate. Merchant-configured amounts (fixed-amount discounts, flat prices, volume tier amounts) are stored in the shop's store currency; on a "local currencies" market, pass the buyer's rate (on a Liquid-adjacent page, `window.Shopify.currency.rate`) so quoted sale prices match what checkout charges. Defaults to `1` (no conversion); invalid or non-positive values also fall back to `1`. Percentages are unaffected. |
69
+
70
+ A market-restricted bundle renders when any signal matches: `country` vs the bundle's projected country codes, the resolved `buyer` property's `companyLocationId` vs its projected company locations, or `market-id` vs its market allow-list.
68
71
 
69
72
  **Product resolution** (when `bundle-gid` is absent): explicit `product-handle` → `<meta name="shopify:product-handle">` → `/products/<handle>` URL segment.
70
73
 
package/dist/index.cjs CHANGED
@@ -43,6 +43,8 @@ function intlFormatMoney(currencyCode) {
43
43
  }
44
44
  var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
45
45
  var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
46
+ var BUNDLE_ROLE_ATTRIBUTE = "_lime_bundle_role";
47
+ var BUNDLE_ROLE_GET = "get";
46
48
  function bundleLineAttributes(bundleGid, bundleType) {
47
49
  return [
48
50
  { key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
@@ -3170,7 +3172,8 @@ function buildBogoCartItems(sides, percent, buyQty, getQty) {
3170
3172
  {
3171
3173
  variantId: Number(sides[1].selectedVariantId),
3172
3174
  quantity: getQty,
3173
- priceCents: getPriceCents
3175
+ priceCents: getPriceCents,
3176
+ attributes: [{ key: BUNDLE_ROLE_ATTRIBUTE, value: BUNDLE_ROLE_GET }]
3174
3177
  }
3175
3178
  ];
3176
3179
  }
@@ -3195,18 +3198,20 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3195
3198
  productQuantities: bundle.productQuantities,
3196
3199
  variantQuantities: bundle.variantQuantities
3197
3200
  };
3201
+ const buyAllowed = bundle.buyVariantIds?.length ? bundle.buyVariantIds : null;
3202
+ const getAllowed = bundle.getVariantIds?.length ? bundle.getVariantIds : null;
3198
3203
  const sides = [
3199
3204
  {
3200
3205
  ...adaptProduct(buyProduct, bundle.id, {
3201
3206
  quantities,
3202
- allowedVariantIds: bundle.selectedVariantIds?.[0] ?? null
3207
+ allowedVariantIds: buyAllowed
3203
3208
  }),
3204
3209
  role: "buy"
3205
3210
  },
3206
3211
  {
3207
3212
  ...adaptProduct(getProduct, bundle.id, {
3208
3213
  quantities,
3209
- allowedVariantIds: bundle.selectedVariantIds?.[1] ?? null
3214
+ allowedVariantIds: getAllowed
3210
3215
  }),
3211
3216
  role: "get"
3212
3217
  }
@@ -3268,7 +3273,12 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3268
3273
  buildBogoCartItems(sides, percent, buyQty, getQty).map((item) => ({
3269
3274
  merchandiseId: "gid://shopify/ProductVariant/" + item.variantId,
3270
3275
  quantity: item.quantity,
3271
- attributes: bundleLineAttributes(bundle.id, bundle.bundleType)
3276
+ // Per-line extras (the reward marker) on top of the attribution
3277
+ // every line carries.
3278
+ attributes: [
3279
+ ...bundleLineAttributes(bundle.id, bundle.bundleType),
3280
+ ...item.attributes ?? []
3281
+ ]
3272
3282
  }))
3273
3283
  );
3274
3284
  });
@@ -7212,7 +7222,8 @@ var LimeBundleElement = class extends HTMLElement {
7212
7222
  "locale",
7213
7223
  "country",
7214
7224
  "language",
7215
- "market-id"
7225
+ "market-id",
7226
+ "currency-rate"
7216
7227
  ];
7217
7228
  /**
7218
7229
  * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
@@ -7267,7 +7278,7 @@ var LimeBundleElement = class extends HTMLElement {
7267
7278
  }
7268
7279
  attributeChangedCallback(name, oldValue, newValue) {
7269
7280
  if (oldValue === newValue || !this.isConnected) return;
7270
- if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
7281
+ if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token" || name === "currency-rate") {
7271
7282
  if (this.shopDomain && this.storefrontToken) {
7272
7283
  this.fetchBundle();
7273
7284
  }
@@ -7310,6 +7321,17 @@ var LimeBundleElement = class extends HTMLElement {
7310
7321
  get marketId() {
7311
7322
  return this.getAttribute("market-id") ?? void 0;
7312
7323
  }
7324
+ /**
7325
+ * Store→presentment currency rate. Merchant-configured amounts
7326
+ * (fixed_amount / flat_price values, volume tier amounts) are stored in
7327
+ * the shop's store currency; set this to the buyer's currency rate so
7328
+ * quoted sale prices match what the checkout's discount function
7329
+ * charges. Anything unparseable or non-positive falls back to 1 —
7330
+ * no conversion — via `normalizeCurrencyRate`.
7331
+ */
7332
+ get currencyRate() {
7333
+ return this.getAttribute("currency-rate") ?? void 0;
7334
+ }
7313
7335
  /**
7314
7336
  * Returns the @inContext-wrapped query when any context field is set;
7315
7337
  * otherwise the plain query. Keeps responses publicly cacheable when
@@ -7324,14 +7346,23 @@ var LimeBundleElement = class extends HTMLElement {
7324
7346
  buyer: this.buyer
7325
7347
  }) ? (0, import_core6.withInContext)(query) : query;
7326
7348
  }
7349
+ /** Buyer resolved once per fetch so the market gate can read the
7350
+ * company location without invoking a callback resolver twice. */
7351
+ resolvedBuyer = void 0;
7327
7352
  /**
7328
- * Returns true if this bundle should be hidden for the current market.
7353
+ * Returns true if this bundle should be hidden for the current buyer.
7329
7354
  * For "all" bundles, always returns false (visible). For "specific"
7330
- * bundles, returns true when no marketId was set or when the bundle's
7331
- * marketIds doesn't include the configured market.
7355
+ * bundles, visibility needs a matching signal: the `country` attribute
7356
+ * against the bundle's projected country codes (retail markets), the
7357
+ * resolved buyer's companyLocationId against its projected company
7358
+ * locations (B2B markets), or a legacy `market-id` match.
7332
7359
  */
7333
7360
  isMarketHidden(bundle) {
7334
- return !(0, import_core6.isVisibleInMarket)(bundle, this.marketId);
7361
+ return !(0, import_core6.isVisibleToBuyer)(bundle, {
7362
+ marketId: this.marketId,
7363
+ countryCode: this.country,
7364
+ companyLocationId: this.resolvedBuyer?.companyLocationId
7365
+ });
7335
7366
  }
7336
7367
  async fetchBundle() {
7337
7368
  if (!this.shopDomain || !this.storefrontToken) {
@@ -7344,12 +7375,17 @@ var LimeBundleElement = class extends HTMLElement {
7344
7375
  const controller = new AbortController();
7345
7376
  this.abortController = controller;
7346
7377
  this.renderLoading();
7378
+ try {
7379
+ this.resolvedBuyer = typeof this.buyer === "function" ? await this.buyer() : this.buyer;
7380
+ } catch {
7381
+ this.resolvedBuyer = void 0;
7382
+ }
7347
7383
  const client = (0, import_core6.createStorefrontClient)({
7348
7384
  shopDomain: this.shopDomain,
7349
7385
  accessToken: this.storefrontToken,
7350
7386
  country: this.country,
7351
7387
  language: this.language,
7352
- buyer: this.buyer
7388
+ buyer: this.resolvedBuyer
7353
7389
  });
7354
7390
  try {
7355
7391
  let bundlePromise;
@@ -7414,7 +7450,7 @@ var LimeBundleElement = class extends HTMLElement {
7414
7450
  data.metaobject.id,
7415
7451
  data.metaobject.fields
7416
7452
  );
7417
- this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];
7453
+ this.bundles = parsed && !this.isMarketHidden(parsed) ? [(0, import_core6.convertBundleToPresentment)(parsed, this.currencyRate)] : [];
7418
7454
  }
7419
7455
  async fetchProductBundles(client, signal, productHandle) {
7420
7456
  const data = await client.query(
@@ -7430,7 +7466,9 @@ var LimeBundleElement = class extends HTMLElement {
7430
7466
  const bundles = [];
7431
7467
  for (const ref of refs) {
7432
7468
  const parsed = (0, import_core6.parseMetaobjectBundle)(ref.id, ref.fields);
7433
- if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
7469
+ if (parsed && !this.isMarketHidden(parsed)) {
7470
+ bundles.push((0, import_core6.convertBundleToPresentment)(parsed, this.currencyRate));
7471
+ }
7434
7472
  }
7435
7473
  this.bundles = (0, import_core6.resolveAbTests)(bundles, (0, import_core6.getVisitorId)());
7436
7474
  }
package/dist/index.d.cts CHANGED
@@ -109,17 +109,31 @@ declare class LimeBundleElement extends HTMLElement {
109
109
  private get country();
110
110
  private get language();
111
111
  private get marketId();
112
+ /**
113
+ * Store→presentment currency rate. Merchant-configured amounts
114
+ * (fixed_amount / flat_price values, volume tier amounts) are stored in
115
+ * the shop's store currency; set this to the buyer's currency rate so
116
+ * quoted sale prices match what the checkout's discount function
117
+ * charges. Anything unparseable or non-positive falls back to 1 —
118
+ * no conversion — via `normalizeCurrencyRate`.
119
+ */
120
+ private get currencyRate();
112
121
  /**
113
122
  * Returns the @inContext-wrapped query when any context field is set;
114
123
  * otherwise the plain query. Keeps responses publicly cacheable when
115
124
  * no buyer/country/language is set.
116
125
  */
117
126
  private wrapQueryForContext;
127
+ /** Buyer resolved once per fetch so the market gate can read the
128
+ * company location without invoking a callback resolver twice. */
129
+ private resolvedBuyer;
118
130
  /**
119
- * Returns true if this bundle should be hidden for the current market.
131
+ * Returns true if this bundle should be hidden for the current buyer.
120
132
  * For "all" bundles, always returns false (visible). For "specific"
121
- * bundles, returns true when no marketId was set or when the bundle's
122
- * marketIds doesn't include the configured market.
133
+ * bundles, visibility needs a matching signal: the `country` attribute
134
+ * against the bundle's projected country codes (retail markets), the
135
+ * resolved buyer's companyLocationId against its projected company
136
+ * locations (B2B markets), or a legacy `market-id` match.
123
137
  */
124
138
  private isMarketHidden;
125
139
  private fetchBundle;
package/dist/index.d.ts CHANGED
@@ -109,17 +109,31 @@ declare class LimeBundleElement extends HTMLElement {
109
109
  private get country();
110
110
  private get language();
111
111
  private get marketId();
112
+ /**
113
+ * Store→presentment currency rate. Merchant-configured amounts
114
+ * (fixed_amount / flat_price values, volume tier amounts) are stored in
115
+ * the shop's store currency; set this to the buyer's currency rate so
116
+ * quoted sale prices match what the checkout's discount function
117
+ * charges. Anything unparseable or non-positive falls back to 1 —
118
+ * no conversion — via `normalizeCurrencyRate`.
119
+ */
120
+ private get currencyRate();
112
121
  /**
113
122
  * Returns the @inContext-wrapped query when any context field is set;
114
123
  * otherwise the plain query. Keeps responses publicly cacheable when
115
124
  * no buyer/country/language is set.
116
125
  */
117
126
  private wrapQueryForContext;
127
+ /** Buyer resolved once per fetch so the market gate can read the
128
+ * company location without invoking a callback resolver twice. */
129
+ private resolvedBuyer;
118
130
  /**
119
- * Returns true if this bundle should be hidden for the current market.
131
+ * Returns true if this bundle should be hidden for the current buyer.
120
132
  * For "all" bundles, always returns false (visible). For "specific"
121
- * bundles, returns true when no marketId was set or when the bundle's
122
- * marketIds doesn't include the configured market.
133
+ * bundles, visibility needs a matching signal: the `country` attribute
134
+ * against the bundle's projected country codes (retail markets), the
135
+ * resolved buyer's companyLocationId against its projected company
136
+ * locations (B2B markets), or a legacy `market-id` match.
123
137
  */
124
138
  private isMarketHidden;
125
139
  private fetchBundle;
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  CART_LINES_REMOVE_MUTATION,
10
10
  SHOP_SETTINGS_QUERY,
11
11
  parseMetaobjectBundle,
12
+ convertBundleToPresentment,
12
13
  observeImpression,
13
14
  reportImpression,
14
15
  reportAddToCart,
@@ -16,7 +17,7 @@ import {
16
17
  sanitizeCustomCss,
17
18
  hasInContext,
18
19
  withInContext,
19
- isVisibleInMarket,
20
+ isVisibleToBuyer,
20
21
  getVisitorId,
21
22
  resolveAbTests
22
23
  } from "@lime-bundles/core";
@@ -36,6 +37,8 @@ function intlFormatMoney(currencyCode) {
36
37
  }
37
38
  var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
38
39
  var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
40
+ var BUNDLE_ROLE_ATTRIBUTE = "_lime_bundle_role";
41
+ var BUNDLE_ROLE_GET = "get";
39
42
  function bundleLineAttributes(bundleGid, bundleType) {
40
43
  return [
41
44
  { key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
@@ -3175,7 +3178,8 @@ function buildBogoCartItems(sides, percent, buyQty, getQty) {
3175
3178
  {
3176
3179
  variantId: Number(sides[1].selectedVariantId),
3177
3180
  quantity: getQty,
3178
- priceCents: getPriceCents
3181
+ priceCents: getPriceCents,
3182
+ attributes: [{ key: BUNDLE_ROLE_ATTRIBUTE, value: BUNDLE_ROLE_GET }]
3179
3183
  }
3180
3184
  ];
3181
3185
  }
@@ -3200,18 +3204,20 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3200
3204
  productQuantities: bundle.productQuantities,
3201
3205
  variantQuantities: bundle.variantQuantities
3202
3206
  };
3207
+ const buyAllowed = bundle.buyVariantIds?.length ? bundle.buyVariantIds : null;
3208
+ const getAllowed = bundle.getVariantIds?.length ? bundle.getVariantIds : null;
3203
3209
  const sides = [
3204
3210
  {
3205
3211
  ...adaptProduct(buyProduct, bundle.id, {
3206
3212
  quantities,
3207
- allowedVariantIds: bundle.selectedVariantIds?.[0] ?? null
3213
+ allowedVariantIds: buyAllowed
3208
3214
  }),
3209
3215
  role: "buy"
3210
3216
  },
3211
3217
  {
3212
3218
  ...adaptProduct(getProduct, bundle.id, {
3213
3219
  quantities,
3214
- allowedVariantIds: bundle.selectedVariantIds?.[1] ?? null
3220
+ allowedVariantIds: getAllowed
3215
3221
  }),
3216
3222
  role: "get"
3217
3223
  }
@@ -3273,7 +3279,12 @@ function renderBogoBundle(container, bundle, onAddToCart, onCleanup) {
3273
3279
  buildBogoCartItems(sides, percent, buyQty, getQty).map((item) => ({
3274
3280
  merchandiseId: "gid://shopify/ProductVariant/" + item.variantId,
3275
3281
  quantity: item.quantity,
3276
- attributes: bundleLineAttributes(bundle.id, bundle.bundleType)
3282
+ // Per-line extras (the reward marker) on top of the attribution
3283
+ // every line carries.
3284
+ attributes: [
3285
+ ...bundleLineAttributes(bundle.id, bundle.bundleType),
3286
+ ...item.attributes ?? []
3287
+ ]
3277
3288
  }))
3278
3289
  );
3279
3290
  });
@@ -7220,7 +7231,8 @@ var LimeBundleElement = class extends HTMLElement {
7220
7231
  "locale",
7221
7232
  "country",
7222
7233
  "language",
7223
- "market-id"
7234
+ "market-id",
7235
+ "currency-rate"
7224
7236
  ];
7225
7237
  /**
7226
7238
  * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
@@ -7275,7 +7287,7 @@ var LimeBundleElement = class extends HTMLElement {
7275
7287
  }
7276
7288
  attributeChangedCallback(name, oldValue, newValue) {
7277
7289
  if (oldValue === newValue || !this.isConnected) return;
7278
- if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
7290
+ if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token" || name === "currency-rate") {
7279
7291
  if (this.shopDomain && this.storefrontToken) {
7280
7292
  this.fetchBundle();
7281
7293
  }
@@ -7318,6 +7330,17 @@ var LimeBundleElement = class extends HTMLElement {
7318
7330
  get marketId() {
7319
7331
  return this.getAttribute("market-id") ?? void 0;
7320
7332
  }
7333
+ /**
7334
+ * Store→presentment currency rate. Merchant-configured amounts
7335
+ * (fixed_amount / flat_price values, volume tier amounts) are stored in
7336
+ * the shop's store currency; set this to the buyer's currency rate so
7337
+ * quoted sale prices match what the checkout's discount function
7338
+ * charges. Anything unparseable or non-positive falls back to 1 —
7339
+ * no conversion — via `normalizeCurrencyRate`.
7340
+ */
7341
+ get currencyRate() {
7342
+ return this.getAttribute("currency-rate") ?? void 0;
7343
+ }
7321
7344
  /**
7322
7345
  * Returns the @inContext-wrapped query when any context field is set;
7323
7346
  * otherwise the plain query. Keeps responses publicly cacheable when
@@ -7332,14 +7355,23 @@ var LimeBundleElement = class extends HTMLElement {
7332
7355
  buyer: this.buyer
7333
7356
  }) ? withInContext(query) : query;
7334
7357
  }
7358
+ /** Buyer resolved once per fetch so the market gate can read the
7359
+ * company location without invoking a callback resolver twice. */
7360
+ resolvedBuyer = void 0;
7335
7361
  /**
7336
- * Returns true if this bundle should be hidden for the current market.
7362
+ * Returns true if this bundle should be hidden for the current buyer.
7337
7363
  * For "all" bundles, always returns false (visible). For "specific"
7338
- * bundles, returns true when no marketId was set or when the bundle's
7339
- * marketIds doesn't include the configured market.
7364
+ * bundles, visibility needs a matching signal: the `country` attribute
7365
+ * against the bundle's projected country codes (retail markets), the
7366
+ * resolved buyer's companyLocationId against its projected company
7367
+ * locations (B2B markets), or a legacy `market-id` match.
7340
7368
  */
7341
7369
  isMarketHidden(bundle) {
7342
- return !isVisibleInMarket(bundle, this.marketId);
7370
+ return !isVisibleToBuyer(bundle, {
7371
+ marketId: this.marketId,
7372
+ countryCode: this.country,
7373
+ companyLocationId: this.resolvedBuyer?.companyLocationId
7374
+ });
7343
7375
  }
7344
7376
  async fetchBundle() {
7345
7377
  if (!this.shopDomain || !this.storefrontToken) {
@@ -7352,12 +7384,17 @@ var LimeBundleElement = class extends HTMLElement {
7352
7384
  const controller = new AbortController();
7353
7385
  this.abortController = controller;
7354
7386
  this.renderLoading();
7387
+ try {
7388
+ this.resolvedBuyer = typeof this.buyer === "function" ? await this.buyer() : this.buyer;
7389
+ } catch {
7390
+ this.resolvedBuyer = void 0;
7391
+ }
7355
7392
  const client = createStorefrontClient({
7356
7393
  shopDomain: this.shopDomain,
7357
7394
  accessToken: this.storefrontToken,
7358
7395
  country: this.country,
7359
7396
  language: this.language,
7360
- buyer: this.buyer
7397
+ buyer: this.resolvedBuyer
7361
7398
  });
7362
7399
  try {
7363
7400
  let bundlePromise;
@@ -7422,7 +7459,7 @@ var LimeBundleElement = class extends HTMLElement {
7422
7459
  data.metaobject.id,
7423
7460
  data.metaobject.fields
7424
7461
  );
7425
- this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];
7462
+ this.bundles = parsed && !this.isMarketHidden(parsed) ? [convertBundleToPresentment(parsed, this.currencyRate)] : [];
7426
7463
  }
7427
7464
  async fetchProductBundles(client, signal, productHandle) {
7428
7465
  const data = await client.query(
@@ -7438,7 +7475,9 @@ var LimeBundleElement = class extends HTMLElement {
7438
7475
  const bundles = [];
7439
7476
  for (const ref of refs) {
7440
7477
  const parsed = parseMetaobjectBundle(ref.id, ref.fields);
7441
- if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
7478
+ if (parsed && !this.isMarketHidden(parsed)) {
7479
+ bundles.push(convertBundleToPresentment(parsed, this.currencyRate));
7480
+ }
7442
7481
  }
7443
7482
  this.bundles = resolveAbTests(bundles, getVisitorId());
7444
7483
  }