@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/dist/index.d.cts CHANGED
@@ -1,5 +1,64 @@
1
+ import { BuyerResolver } from '@lime-bundles/core';
2
+
3
+ /**
4
+ * <lime-bundle> Web Component — renders Lime Bundles on any storefront.
5
+ *
6
+ * ## The two modes
7
+ *
8
+ * Single-bundle (pinned):
9
+ * <lime-bundle
10
+ * shop-domain="my-shop.myshopify.com"
11
+ * storefront-token="shpat_..."
12
+ * bundle-gid="gid://shopify/Metaobject/42"
13
+ * ></lime-bundle>
14
+ *
15
+ * Product-aware (matches classic Liquid theme block behaviour — one snippet
16
+ * on the product page renders every active bundle configured against that
17
+ * product):
18
+ * <lime-bundle
19
+ * shop-domain="my-shop.myshopify.com"
20
+ * storefront-token="shpat_..."
21
+ * ></lime-bundle>
22
+ *
23
+ * Product resolution cascade (when no `bundle-gid` is set):
24
+ * 1. explicit `product-handle` attribute
25
+ * 2. <meta name="shopify:product-handle" content="..."> on the page
26
+ * 3. /products/<handle> segment of window.location.pathname
27
+ * 4. fallthrough: renders nothing, fires `lime-bundle:error`
28
+ *
29
+ * ## Add-to-cart behaviour
30
+ *
31
+ * Merchants who do nothing get a default: the widget calls Shopify's
32
+ * Storefront Cart API (tokenless — no extra scopes required) and redirects
33
+ * the browser to the returned checkoutUrl. One-click-to-checkout is the
34
+ * right UX for most merchants pasting the widget into Webflow / Wix /
35
+ * Squarespace / static HTML.
36
+ *
37
+ * Merchants with their own cart state (Hydrogen's useCart, a custom cart
38
+ * drawer, etc.) opt out by attaching a listener that calls
39
+ * `event.preventDefault()`:
40
+ *
41
+ * document.querySelector("lime-bundle").addEventListener(
42
+ * "lime-bundle:add-to-cart",
43
+ * (ev) => {
44
+ * ev.preventDefault(); // suppress the default redirect
45
+ * myCart.linesAdd(ev.detail.lines);
46
+ * },
47
+ * );
48
+ *
49
+ * The event is always dispatched; only the default action is conditional.
50
+ */
51
+
1
52
  declare class LimeBundleElement extends HTMLElement {
2
53
  static observedAttributes: string[];
54
+ /**
55
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
56
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
57
+ * because the customer access token would land in DOM snapshots (Sentry,
58
+ * analytics, browser extensions) and Referer headers. See the package
59
+ * README ("Markets & B2B").
60
+ */
61
+ buyer: BuyerResolver | undefined;
3
62
  private shadow;
4
63
  private bundles;
5
64
  private abortController;
@@ -28,16 +87,38 @@ declare class LimeBundleElement extends HTMLElement {
28
87
  private get productHandleAttr();
29
88
  private get appUrl();
30
89
  private get analyticsEnabled();
90
+ private get country();
91
+ private get language();
92
+ private get marketId();
93
+ /**
94
+ * Returns the @inContext-wrapped query when any context field is set;
95
+ * otherwise the plain query. Keeps responses publicly cacheable when
96
+ * no buyer/country/language is set.
97
+ */
98
+ private wrapQueryForContext;
99
+ /**
100
+ * Returns true if this bundle should be hidden for the current market.
101
+ * For "all" bundles, always returns false (visible). For "specific"
102
+ * bundles, returns true when no marketId was set or when the bundle's
103
+ * marketIds doesn't include the configured market.
104
+ */
105
+ private isMarketHidden;
31
106
  private fetchBundle;
32
107
  /**
33
- * Resolve the visitor's A/B bucket for every bundle with an active test
34
- * and merge Variant B overrides where applicable. Runs in parallel; any
35
- * assignment failure logs internally but still renders Variant A (safe
36
- * default). The `getABTestAssignment` helper persists the bucket via a
37
- * first-party cookie; variant attribution is recorded server-side from
38
- * the analytics events the widget already emits.
108
+ * Resolve the visitor's assigned variant for every primary bundle in a link
109
+ * group. Runs in parallel; any assignment failure logs internally and still
110
+ * renders the primary (safe default). The `getLinkGroupAssignment` helper
111
+ * persists the bucket via a first-party cookie; variant attribution is
112
+ * recorded server-side from the cart-line `_lime_bundle_gid` + `_lime_link_group`
113
+ * attributes the cart-add path stamps when an assignment lands on a non-primary
114
+ * variant.
115
+ *
116
+ * When the assignment points to a non-primary variant the SDK swaps in that
117
+ * variant's `ParsedBundle` from the primary's `linkGroup.variants` list. The
118
+ * variant data must already be present in the storefront query response — the
119
+ * SDK does NOT issue a separate metaobject fetch on the hot path.
39
120
  */
40
- private applyABVariants;
121
+ private applyLinkGroupVariants;
41
122
  private fetchSingleBundle;
42
123
  private fetchProductBundles;
43
124
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,64 @@
1
+ import { BuyerResolver } from '@lime-bundles/core';
2
+
3
+ /**
4
+ * <lime-bundle> Web Component — renders Lime Bundles on any storefront.
5
+ *
6
+ * ## The two modes
7
+ *
8
+ * Single-bundle (pinned):
9
+ * <lime-bundle
10
+ * shop-domain="my-shop.myshopify.com"
11
+ * storefront-token="shpat_..."
12
+ * bundle-gid="gid://shopify/Metaobject/42"
13
+ * ></lime-bundle>
14
+ *
15
+ * Product-aware (matches classic Liquid theme block behaviour — one snippet
16
+ * on the product page renders every active bundle configured against that
17
+ * product):
18
+ * <lime-bundle
19
+ * shop-domain="my-shop.myshopify.com"
20
+ * storefront-token="shpat_..."
21
+ * ></lime-bundle>
22
+ *
23
+ * Product resolution cascade (when no `bundle-gid` is set):
24
+ * 1. explicit `product-handle` attribute
25
+ * 2. <meta name="shopify:product-handle" content="..."> on the page
26
+ * 3. /products/<handle> segment of window.location.pathname
27
+ * 4. fallthrough: renders nothing, fires `lime-bundle:error`
28
+ *
29
+ * ## Add-to-cart behaviour
30
+ *
31
+ * Merchants who do nothing get a default: the widget calls Shopify's
32
+ * Storefront Cart API (tokenless — no extra scopes required) and redirects
33
+ * the browser to the returned checkoutUrl. One-click-to-checkout is the
34
+ * right UX for most merchants pasting the widget into Webflow / Wix /
35
+ * Squarespace / static HTML.
36
+ *
37
+ * Merchants with their own cart state (Hydrogen's useCart, a custom cart
38
+ * drawer, etc.) opt out by attaching a listener that calls
39
+ * `event.preventDefault()`:
40
+ *
41
+ * document.querySelector("lime-bundle").addEventListener(
42
+ * "lime-bundle:add-to-cart",
43
+ * (ev) => {
44
+ * ev.preventDefault(); // suppress the default redirect
45
+ * myCart.linesAdd(ev.detail.lines);
46
+ * },
47
+ * );
48
+ *
49
+ * The event is always dispatched; only the default action is conditional.
50
+ */
51
+
1
52
  declare class LimeBundleElement extends HTMLElement {
2
53
  static observedAttributes: string[];
54
+ /**
55
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
56
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
57
+ * because the customer access token would land in DOM snapshots (Sentry,
58
+ * analytics, browser extensions) and Referer headers. See the package
59
+ * README ("Markets & B2B").
60
+ */
61
+ buyer: BuyerResolver | undefined;
3
62
  private shadow;
4
63
  private bundles;
5
64
  private abortController;
@@ -28,16 +87,38 @@ declare class LimeBundleElement extends HTMLElement {
28
87
  private get productHandleAttr();
29
88
  private get appUrl();
30
89
  private get analyticsEnabled();
90
+ private get country();
91
+ private get language();
92
+ private get marketId();
93
+ /**
94
+ * Returns the @inContext-wrapped query when any context field is set;
95
+ * otherwise the plain query. Keeps responses publicly cacheable when
96
+ * no buyer/country/language is set.
97
+ */
98
+ private wrapQueryForContext;
99
+ /**
100
+ * Returns true if this bundle should be hidden for the current market.
101
+ * For "all" bundles, always returns false (visible). For "specific"
102
+ * bundles, returns true when no marketId was set or when the bundle's
103
+ * marketIds doesn't include the configured market.
104
+ */
105
+ private isMarketHidden;
31
106
  private fetchBundle;
32
107
  /**
33
- * Resolve the visitor's A/B bucket for every bundle with an active test
34
- * and merge Variant B overrides where applicable. Runs in parallel; any
35
- * assignment failure logs internally but still renders Variant A (safe
36
- * default). The `getABTestAssignment` helper persists the bucket via a
37
- * first-party cookie; variant attribution is recorded server-side from
38
- * the analytics events the widget already emits.
108
+ * Resolve the visitor's assigned variant for every primary bundle in a link
109
+ * group. Runs in parallel; any assignment failure logs internally and still
110
+ * renders the primary (safe default). The `getLinkGroupAssignment` helper
111
+ * persists the bucket via a first-party cookie; variant attribution is
112
+ * recorded server-side from the cart-line `_lime_bundle_gid` + `_lime_link_group`
113
+ * attributes the cart-add path stamps when an assignment lands on a non-primary
114
+ * variant.
115
+ *
116
+ * When the assignment points to a non-primary variant the SDK swaps in that
117
+ * variant's `ParsedBundle` from the primary's `linkGroup.variants` list. The
118
+ * variant data must already be present in the storefront query response — the
119
+ * SDK does NOT issue a separate metaobject fetch on the hot path.
39
120
  */
40
- private applyABVariants;
121
+ private applyLinkGroupVariants;
41
122
  private fetchSingleBundle;
42
123
  private fetchProductBundles;
43
124
  /**
package/dist/index.js CHANGED
@@ -12,8 +12,9 @@ import {
12
12
  reportAddToCart,
13
13
  injectCustomCss,
14
14
  sanitizeCustomCss,
15
- getABTestAssignment,
16
- applyABVariantB
15
+ getLinkGroupAssignment,
16
+ hasInContext,
17
+ withInContext
17
18
  } from "@lime-bundles/core";
18
19
 
19
20
  // src/renderers/fixed.ts
@@ -4137,8 +4138,19 @@ var LimeBundleElement = class extends HTMLElement {
4137
4138
  "product-handle",
4138
4139
  "app-url",
4139
4140
  "analytics",
4140
- "locale"
4141
+ "locale",
4142
+ "country",
4143
+ "language",
4144
+ "market-id"
4141
4145
  ];
4146
+ /**
4147
+ * B2B buyer identity. Set programmatically — `element.buyer = {...}` or
4148
+ * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute
4149
+ * because the customer access token would land in DOM snapshots (Sentry,
4150
+ * analytics, browser extensions) and Referer headers. See the package
4151
+ * README ("Markets & B2B").
4152
+ */
4153
+ buyer = void 0;
4142
4154
  shadow;
4143
4155
  bundles = [];
4144
4156
  abortController = null;
@@ -4201,6 +4213,40 @@ var LimeBundleElement = class extends HTMLElement {
4201
4213
  get analyticsEnabled() {
4202
4214
  return this.getAttribute("analytics") !== "false";
4203
4215
  }
4216
+ get country() {
4217
+ return this.getAttribute("country") ?? void 0;
4218
+ }
4219
+ get language() {
4220
+ return this.getAttribute("language") ?? void 0;
4221
+ }
4222
+ get marketId() {
4223
+ return this.getAttribute("market-id") ?? void 0;
4224
+ }
4225
+ /**
4226
+ * Returns the @inContext-wrapped query when any context field is set;
4227
+ * otherwise the plain query. Keeps responses publicly cacheable when
4228
+ * no buyer/country/language is set.
4229
+ */
4230
+ wrapQueryForContext(query) {
4231
+ return hasInContext({
4232
+ shopDomain: this.shopDomain,
4233
+ accessToken: this.storefrontToken,
4234
+ country: this.country,
4235
+ language: this.language,
4236
+ buyer: this.buyer
4237
+ }) ? withInContext(query) : query;
4238
+ }
4239
+ /**
4240
+ * Returns true if this bundle should be hidden for the current market.
4241
+ * For "all" bundles, always returns false (visible). For "specific"
4242
+ * bundles, returns true when no marketId was set or when the bundle's
4243
+ * marketIds doesn't include the configured market.
4244
+ */
4245
+ isMarketHidden(bundle) {
4246
+ if (bundle.marketVisibility !== "specific") return false;
4247
+ if (!this.marketId) return true;
4248
+ return !bundle.marketIds.includes(this.marketId);
4249
+ }
4204
4250
  async fetchBundle() {
4205
4251
  if (!this.shopDomain || !this.storefrontToken) {
4206
4252
  this.renderError(
@@ -4214,7 +4260,10 @@ var LimeBundleElement = class extends HTMLElement {
4214
4260
  this.renderLoading();
4215
4261
  const client = createStorefrontClient({
4216
4262
  shopDomain: this.shopDomain,
4217
- accessToken: this.storefrontToken
4263
+ accessToken: this.storefrontToken,
4264
+ country: this.country,
4265
+ language: this.language,
4266
+ buyer: this.buyer
4218
4267
  });
4219
4268
  try {
4220
4269
  let bundlePromise;
@@ -4253,7 +4302,7 @@ var LimeBundleElement = class extends HTMLElement {
4253
4302
  const sanitized = sanitizeCustomCss(css.shop.metafield.value);
4254
4303
  if (sanitized.ok) this.shopCustomCss = sanitized.css;
4255
4304
  }
4256
- await this.applyABVariants();
4305
+ await this.applyLinkGroupVariants();
4257
4306
  this.renderBundles();
4258
4307
  } catch (err) {
4259
4308
  if (controller.signal.aborted) return;
@@ -4265,40 +4314,54 @@ var LimeBundleElement = class extends HTMLElement {
4265
4314
  }
4266
4315
  }
4267
4316
  /**
4268
- * Resolve the visitor's A/B bucket for every bundle with an active test
4269
- * and merge Variant B overrides where applicable. Runs in parallel; any
4270
- * assignment failure logs internally but still renders Variant A (safe
4271
- * default). The `getABTestAssignment` helper persists the bucket via a
4272
- * first-party cookie; variant attribution is recorded server-side from
4273
- * the analytics events the widget already emits.
4317
+ * Resolve the visitor's assigned variant for every primary bundle in a link
4318
+ * group. Runs in parallel; any assignment failure logs internally and still
4319
+ * renders the primary (safe default). The `getLinkGroupAssignment` helper
4320
+ * persists the bucket via a first-party cookie; variant attribution is
4321
+ * recorded server-side from the cart-line `_lime_bundle_gid` + `_lime_link_group`
4322
+ * attributes the cart-add path stamps when an assignment lands on a non-primary
4323
+ * variant.
4324
+ *
4325
+ * When the assignment points to a non-primary variant the SDK swaps in that
4326
+ * variant's `ParsedBundle` from the primary's `linkGroup.variants` list. The
4327
+ * variant data must already be present in the storefront query response — the
4328
+ * SDK does NOT issue a separate metaobject fetch on the hot path.
4274
4329
  */
4275
- async applyABVariants() {
4330
+ async applyLinkGroupVariants() {
4276
4331
  if (this.bundles.length === 0) return;
4332
+ const bundleLookupByMetaobjectId = /* @__PURE__ */ new Map();
4333
+ for (const b of this.bundles) {
4334
+ bundleLookupByMetaobjectId.set(b.id, b);
4335
+ }
4277
4336
  const results = await Promise.all(
4278
4337
  this.bundles.map(async (bundle) => {
4279
- if (!bundle.abTestId || !bundle.abVariantB) return bundle;
4338
+ const lg = bundle.linkGroup;
4339
+ if (!lg || !lg.isPrimary || !lg.variants || lg.variants.length < 2) {
4340
+ return bundle;
4341
+ }
4280
4342
  try {
4281
- const assignment = await getABTestAssignment(
4282
- bundle.abTestId,
4283
- bundle.id
4343
+ const assignment = await getLinkGroupAssignment(lg.id, lg.variants);
4344
+ if (!assignment) return bundle;
4345
+ if (assignment.variantMetaobjectId === bundle.id) return bundle;
4346
+ const swap = bundleLookupByMetaobjectId.get(
4347
+ assignment.variantMetaobjectId
4284
4348
  );
4285
- if (assignment?.variant === "B") {
4286
- return applyABVariantB(bundle);
4287
- }
4349
+ if (!swap) return bundle;
4350
+ return swap;
4288
4351
  } catch (err) {
4289
4352
  console.warn(
4290
- `[lime-bundle] A/B assignment failed for bundle ${bundle.id}; falling back to Variant A.`,
4353
+ `[lime-bundle] link-group assignment failed for bundle ${bundle.id}; falling back to primary.`,
4291
4354
  err
4292
4355
  );
4356
+ return bundle;
4293
4357
  }
4294
- return bundle;
4295
4358
  })
4296
4359
  );
4297
4360
  this.bundles = results;
4298
4361
  }
4299
4362
  async fetchSingleBundle(client, signal) {
4300
4363
  const data = await client.query(
4301
- BUNDLE_METAOBJECT_QUERY,
4364
+ this.wrapQueryForContext(BUNDLE_METAOBJECT_QUERY),
4302
4365
  { id: this.bundleGid },
4303
4366
  { signal }
4304
4367
  );
@@ -4310,11 +4373,11 @@ var LimeBundleElement = class extends HTMLElement {
4310
4373
  data.metaobject.id,
4311
4374
  data.metaobject.fields
4312
4375
  );
4313
- this.bundles = parsed ? [parsed] : [];
4376
+ this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];
4314
4377
  }
4315
4378
  async fetchProductBundles(client, signal, productHandle) {
4316
4379
  const data = await client.query(
4317
- BUNDLES_FOR_PRODUCT_QUERY,
4380
+ this.wrapQueryForContext(BUNDLES_FOR_PRODUCT_QUERY),
4318
4381
  { handle: productHandle },
4319
4382
  { signal }
4320
4383
  );
@@ -4326,7 +4389,7 @@ var LimeBundleElement = class extends HTMLElement {
4326
4389
  const bundles = [];
4327
4390
  for (const ref of refs) {
4328
4391
  const parsed = parseMetaobjectBundle(ref.id, ref.fields);
4329
- if (parsed) bundles.push(parsed);
4392
+ if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);
4330
4393
  }
4331
4394
  this.bundles = bundles;
4332
4395
  }