@lime-bundles/widget 3.2.2 → 3.4.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/README.md CHANGED
@@ -61,11 +61,30 @@ document.querySelector("lime-bundle").addEventListener(
61
61
  | `app-url` | | Enables impression + add-to-cart analytics when set. |
62
62
  | `analytics` | | `"false"` suppresses analytics even when `app-url` is set. |
63
63
  | `locale` | | BCP-47 tag forwarded to the Storefront API. |
64
+ | `country` | | ISO-3166 alpha-2 (e.g. `"US"`). Adds `@inContext(country:)` to Storefront queries — Markets pricing + currency. |
65
+ | `language` | | ISO-639-1 (e.g. `"EN"`). Adds `@inContext(language:)`. |
66
+ | `market-id` | | Current visitor's Market GID. Hides market-restricted bundles whose allow-list doesn't include this market. |
64
67
 
65
68
  **Product resolution** (when `bundle-gid` is absent): explicit `product-handle` → `<meta name="shopify:product-handle">` → `/products/<handle>` URL segment.
66
69
 
67
70
  Changing any attribute at runtime re-fetches and re-renders.
68
71
 
72
+ ### B2B buyer identity (programmatic property only)
73
+
74
+ `buyer` is **not** an HTML attribute. Set it on the element after creation so the customer access token never lands in DOM-snapshot tools (Sentry, analytics, browser extensions) or Referer headers:
75
+
76
+ ```js
77
+ const el = document.querySelector("lime-bundle");
78
+ el.buyer = async () => {
79
+ // Resolved per request — no token in static markup
80
+ return { customerAccessToken: await fetchToken(), companyLocationId: "gid://shopify/CompanyLocation/9" };
81
+ };
82
+ ```
83
+
84
+ The Customer Account API PKCE flow is the canonical token source. See https://shopify.dev/docs/api/customer/latest.
85
+
86
+ Buyer-contextual Storefront responses MUST be `Cache-Control: private` — never share across users.
87
+
69
88
  ## Events
70
89
 
71
90
  All events bubble and pierce shadow-DOM boundaries (`composed: true`), so you can listen on any ancestor.
package/dist/index.cjs CHANGED
@@ -4130,8 +4130,19 @@ var LimeBundleElement = class extends HTMLElement {
4130
4130
  "product-handle",
4131
4131
  "app-url",
4132
4132
  "analytics",
4133
- "locale"
4133
+ "locale",
4134
+ "country",
4135
+ "language",
4136
+ "market-id"
4134
4137
  ];
4138
+ /**
4139
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
4140
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
4141
+ * because the customer access token would land in DOM snapshots (Sentry,
4142
+ * analytics, browser extensions) and Referer headers. See the package
4143
+ * README ("Markets & B2B").
4144
+ */
4145
+ buyer = void 0;
4135
4146
  shadow;
4136
4147
  bundles = [];
4137
4148
  abortController = null;
@@ -4194,6 +4205,40 @@ var LimeBundleElement = class extends HTMLElement {
4194
4205
  get analyticsEnabled() {
4195
4206
  return this.getAttribute("analytics") !== "false";
4196
4207
  }
4208
+ get country() {
4209
+ return this.getAttribute("country") ?? void 0;
4210
+ }
4211
+ get language() {
4212
+ return this.getAttribute("language") ?? void 0;
4213
+ }
4214
+ get marketId() {
4215
+ return this.getAttribute("market-id") ?? void 0;
4216
+ }
4217
+ /**
4218
+ * Returns the @inContext-wrapped query when any context field is set;
4219
+ * otherwise the plain query. Keeps responses publicly cacheable when
4220
+ * no buyer/country/language is set.
4221
+ */
4222
+ wrapQueryForContext(query) {
4223
+ return (0, import_core8.hasInContext)({
4224
+ shopDomain: this.shopDomain,
4225
+ accessToken: this.storefrontToken,
4226
+ country: this.country,
4227
+ language: this.language,
4228
+ buyer: this.buyer
4229
+ }) ? (0, import_core8.withInContext)(query) : query;
4230
+ }
4231
+ /**
4232
+ * Returns true if this bundle should be hidden for the current market.
4233
+ * For "all" bundles, always returns false (visible). For "specific"
4234
+ * bundles, returns true when no marketId was set or when the bundle's
4235
+ * marketIds doesn't include the configured market.
4236
+ */
4237
+ isMarketHidden(bundle) {
4238
+ if (bundle.marketVisibility !== "specific") return false;
4239
+ if (!this.marketId) return true;
4240
+ return !bundle.marketIds.includes(this.marketId);
4241
+ }
4197
4242
  async fetchBundle() {
4198
4243
  if (!this.shopDomain || !this.storefrontToken) {
4199
4244
  this.renderError(
@@ -4207,7 +4252,10 @@ var LimeBundleElement = class extends HTMLElement {
4207
4252
  this.renderLoading();
4208
4253
  const client = (0, import_core8.createStorefrontClient)({
4209
4254
  shopDomain: this.shopDomain,
4210
- accessToken: this.storefrontToken
4255
+ accessToken: this.storefrontToken,
4256
+ country: this.country,
4257
+ language: this.language,
4258
+ buyer: this.buyer
4211
4259
  });
4212
4260
  try {
4213
4261
  let bundlePromise;
@@ -4246,7 +4294,7 @@ var LimeBundleElement = class extends HTMLElement {
4246
4294
  const sanitized = (0, import_core8.sanitizeCustomCss)(css.shop.metafield.value);
4247
4295
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
4248
4296
  }
4249
- await this.applyABVariants();
4297
+ await this.applyLinkGroupVariants();
4250
4298
  this.renderBundles();
4251
4299
  } catch (err) {
4252
4300
  if (controller.signal.aborted) return;
@@ -4258,40 +4306,54 @@ var LimeBundleElement = class extends HTMLElement {
4258
4306
  }
4259
4307
  }
4260
4308
  /**
4261
- * Resolve the visitor's A/B bucket for every bundle with an active test
4262
- * and merge Variant B overrides where applicable. Runs in parallel; any
4263
- * assignment failure logs internally but still renders Variant A (safe
4264
- * default). The `getABTestAssignment` helper persists the bucket via a
4265
- * first-party cookie; variant attribution is recorded server-side from
4266
- * the analytics events the widget already emits.
4309
+ * Resolve the visitor's assigned variant for every primary bundle in a link
4310
+ * group. Runs in parallel; any assignment failure logs internally and still
4311
+ * renders the primary (safe default). The `getLinkGroupAssignment` helper
4312
+ * persists the bucket via a first-party cookie; variant attribution is
4313
+ * recorded server-side from the cart-line `_lime_bundle_gid` + `_lime_link_group`
4314
+ * attributes the cart-add path stamps when an assignment lands on a non-primary
4315
+ * variant.
4316
+ *
4317
+ * When the assignment points to a non-primary variant the SDK swaps in that
4318
+ * variant's `ParsedBundle` from the primary's `linkGroup.variants` list. The
4319
+ * variant data must already be present in the storefront query response — the
4320
+ * SDK does NOT issue a separate metaobject fetch on the hot path.
4267
4321
  */
4268
- async applyABVariants() {
4322
+ async applyLinkGroupVariants() {
4269
4323
  if (this.bundles.length === 0) return;
4324
+ const bundleLookupByMetaobjectId = /* @__PURE__ */ new Map();
4325
+ for (const b of this.bundles) {
4326
+ bundleLookupByMetaobjectId.set(b.id, b);
4327
+ }
4270
4328
  const results = await Promise.all(
4271
4329
  this.bundles.map(async (bundle) => {
4272
- if (!bundle.abTestId || !bundle.abVariantB) return bundle;
4330
+ const lg = bundle.linkGroup;
4331
+ if (!lg || !lg.isPrimary || !lg.variants || lg.variants.length < 2) {
4332
+ return bundle;
4333
+ }
4273
4334
  try {
4274
- const assignment = await (0, import_core8.getABTestAssignment)(
4275
- bundle.abTestId,
4276
- bundle.id
4335
+ const assignment = await (0, import_core8.getLinkGroupAssignment)(lg.id, lg.variants);
4336
+ if (!assignment) return bundle;
4337
+ if (assignment.variantMetaobjectId === bundle.id) return bundle;
4338
+ const swap = bundleLookupByMetaobjectId.get(
4339
+ assignment.variantMetaobjectId
4277
4340
  );
4278
- if (assignment?.variant === "B") {
4279
- return (0, import_core8.applyABVariantB)(bundle);
4280
- }
4341
+ if (!swap) return bundle;
4342
+ return swap;
4281
4343
  } catch (err) {
4282
4344
  console.warn(
4283
- `[lime-bundle] A/B assignment failed for bundle ${bundle.id}; falling back to Variant A.`,
4345
+ `[lime-bundle] link-group assignment failed for bundle ${bundle.id}; falling back to primary.`,
4284
4346
  err
4285
4347
  );
4348
+ return bundle;
4286
4349
  }
4287
- return bundle;
4288
4350
  })
4289
4351
  );
4290
4352
  this.bundles = results;
4291
4353
  }
4292
4354
  async fetchSingleBundle(client, signal) {
4293
4355
  const data = await client.query(
4294
- import_core8.BUNDLE_METAOBJECT_QUERY,
4356
+ this.wrapQueryForContext(import_core8.BUNDLE_METAOBJECT_QUERY),
4295
4357
  { id: this.bundleGid },
4296
4358
  { signal }
4297
4359
  );
@@ -4303,11 +4365,11 @@ var LimeBundleElement = class extends HTMLElement {
4303
4365
  data.metaobject.id,
4304
4366
  data.metaobject.fields
4305
4367
  );
4306
- this.bundles = parsed ? [parsed] : [];
4368
+ this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];
4307
4369
  }
4308
4370
  async fetchProductBundles(client, signal, productHandle) {
4309
4371
  const data = await client.query(
4310
- import_core8.BUNDLES_FOR_PRODUCT_QUERY,
4372
+ this.wrapQueryForContext(import_core8.BUNDLES_FOR_PRODUCT_QUERY),
4311
4373
  { handle: productHandle },
4312
4374
  { signal }
4313
4375
  );
@@ -4319,7 +4381,7 @@ var LimeBundleElement = class extends HTMLElement {
4319
4381
  const bundles = [];
4320
4382
  for (const ref of refs) {
4321
4383
  const parsed = (0, import_core8.parseMetaobjectBundle)(ref.id, ref.fields);
4322
- if (parsed) bundles.push(parsed);
4384
+ if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
4323
4385
  }
4324
4386
  this.bundles = bundles;
4325
4387
  }