@lime-bundles/widget 0.1.0 → 0.2.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
@@ -7,16 +7,20 @@ declare class LimeBundleElement extends HTMLElement {
7
7
  constructor();
8
8
  connectedCallback(): void;
9
9
  disconnectedCallback(): void;
10
- /** Clean up stale bundle state and active impression observer. */
11
10
  private teardownImpression;
12
11
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
13
12
  private get shopDomain();
14
13
  private get storefrontToken();
15
14
  private get bundleGid();
16
- private get cartId();
17
15
  private get appUrl();
18
16
  private get analyticsEnabled();
19
17
  private fetchBundle;
18
+ /**
19
+ * Dispatch add-to-cart for merchant handling. Returns true — the widget
20
+ * reports success optimistically. If the merchant's cart mutation fails,
21
+ * they're responsible for surfacing that error in their own UI.
22
+ */
23
+ private dispatchAddToCart;
20
24
  private renderBundle;
21
25
  private setupImpression;
22
26
  private renderLoading;
@@ -24,33 +28,4 @@ declare class LimeBundleElement extends HTMLElement {
24
28
  private render;
25
29
  }
26
30
 
27
- /**
28
- * Thank-you page snippet — fires purchase events for headless storefronts.
29
- *
30
- * Usage:
31
- * <script src="https://unpkg.com/@lime-bundles/widget/dist/lime-thankyou.js"></script>
32
- * <script>
33
- * LimeBundles.trackPurchase({
34
- * shopDomain: 'my-store.myshopify.com',
35
- * orderId: 'gid://shopify/Order/12345',
36
- * lineItems: [
37
- * { bundleGid: '...', bundleType: 'fixed', price: 29.99, quantity: 1 }
38
- * ]
39
- * });
40
- * </script>
41
- */
42
- interface PurchaseLineItem {
43
- bundleGid: string;
44
- bundleType: string;
45
- price: number;
46
- quantity: number;
47
- }
48
- interface TrackPurchaseInput {
49
- shopDomain: string;
50
- orderId: string;
51
- lineItems: PurchaseLineItem[];
52
- appUrl?: string;
53
- }
54
- declare function trackPurchase(input: TrackPurchaseInput): void;
55
-
56
- export { LimeBundleElement, trackPurchase };
31
+ export { LimeBundleElement };
package/dist/index.d.ts CHANGED
@@ -7,16 +7,20 @@ declare class LimeBundleElement extends HTMLElement {
7
7
  constructor();
8
8
  connectedCallback(): void;
9
9
  disconnectedCallback(): void;
10
- /** Clean up stale bundle state and active impression observer. */
11
10
  private teardownImpression;
12
11
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
13
12
  private get shopDomain();
14
13
  private get storefrontToken();
15
14
  private get bundleGid();
16
- private get cartId();
17
15
  private get appUrl();
18
16
  private get analyticsEnabled();
19
17
  private fetchBundle;
18
+ /**
19
+ * Dispatch add-to-cart for merchant handling. Returns true — the widget
20
+ * reports success optimistically. If the merchant's cart mutation fails,
21
+ * they're responsible for surfacing that error in their own UI.
22
+ */
23
+ private dispatchAddToCart;
20
24
  private renderBundle;
21
25
  private setupImpression;
22
26
  private renderLoading;
@@ -24,33 +28,4 @@ declare class LimeBundleElement extends HTMLElement {
24
28
  private render;
25
29
  }
26
30
 
27
- /**
28
- * Thank-you page snippet — fires purchase events for headless storefronts.
29
- *
30
- * Usage:
31
- * <script src="https://unpkg.com/@lime-bundles/widget/dist/lime-thankyou.js"></script>
32
- * <script>
33
- * LimeBundles.trackPurchase({
34
- * shopDomain: 'my-store.myshopify.com',
35
- * orderId: 'gid://shopify/Order/12345',
36
- * lineItems: [
37
- * { bundleGid: '...', bundleType: 'fixed', price: 29.99, quantity: 1 }
38
- * ]
39
- * });
40
- * </script>
41
- */
42
- interface PurchaseLineItem {
43
- bundleGid: string;
44
- bundleType: string;
45
- price: number;
46
- quantity: number;
47
- }
48
- interface TrackPurchaseInput {
49
- shopDomain: string;
50
- orderId: string;
51
- lineItems: PurchaseLineItem[];
52
- appUrl?: string;
53
- }
54
- declare function trackPurchase(input: TrackPurchaseInput): void;
55
-
56
- export { LimeBundleElement, trackPurchase };
31
+ export { LimeBundleElement };
package/dist/index.js CHANGED
@@ -2,18 +2,19 @@
2
2
  import {
3
3
  createStorefrontClient,
4
4
  BUNDLE_METAOBJECT_QUERY,
5
+ SHOP_CUSTOM_CSS_QUERY,
5
6
  parseMetaobjectBundle,
6
- detectCartApi,
7
- createAjaxCartApi,
8
- createStorefrontCartApi,
9
7
  observeImpression,
10
8
  reportImpression,
11
- reportAddToCart
9
+ reportAddToCart,
10
+ injectCustomCss
12
11
  } from "@lime-bundles/core";
13
12
 
14
13
  // src/renderers/fixed.ts
15
- import { formatMoney } from "@lime-bundles/core";
16
- function renderFixedBundle(container, bundle, addToCart) {
14
+ import {
15
+ formatMoney
16
+ } from "@lime-bundles/core";
17
+ function renderFixedBundle(container, bundle, onAddToCart) {
17
18
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
18
19
  const title = document.createElement("h3");
19
20
  title.className = "lb-bundle__title";
@@ -54,23 +55,20 @@ function renderFixedBundle(container, bundle, addToCart) {
54
55
  button.className = "lb-bundle__cta";
55
56
  button.textContent = bundle.widgetConfig.ctaText ?? "Add Bundle to Cart";
56
57
  button.setAttribute("part", "button");
57
- button.addEventListener("click", async () => {
58
- button.disabled = true;
59
- button.textContent = "Adding...";
60
- const items = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
58
+ button.addEventListener("click", () => {
59
+ const lines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
61
60
  const variant = p.variants.nodes.find((v) => v.availableForSale);
62
- return { variantId: variant.id, quantity: 1 };
61
+ return {
62
+ merchandiseId: variant.id,
63
+ quantity: 1,
64
+ attributes: [
65
+ { key: "_lime_bundle_gid", value: bundle.id },
66
+ { key: "_lime_bundle_type", value: bundle.bundleType }
67
+ ]
68
+ };
63
69
  });
64
- const result = await addToCart(items);
65
- button.disabled = false;
66
- button.textContent = bundle.widgetConfig.ctaText ?? "Add Bundle to Cart";
67
- if (!result.success) {
68
- const error = document.createElement("p");
69
- error.className = "lb-bundle__error";
70
- error.textContent = result.error ?? "Failed to add to cart";
71
- container.appendChild(error);
72
- setTimeout(() => error.remove(), 5e3);
73
- }
70
+ if (lines.length === 0) return;
71
+ onAddToCart(lines);
74
72
  });
75
73
  container.appendChild(button);
76
74
  }
@@ -81,8 +79,11 @@ function escapeHtml(str) {
81
79
  }
82
80
 
83
81
  // src/renderers/mix-match.ts
84
- import { formatMoney as formatMoney2, validateQuantity } from "@lime-bundles/core";
85
- function renderMixMatchBundle(container, bundle, addToCart) {
82
+ import {
83
+ formatMoney as formatMoney2,
84
+ validateQuantity
85
+ } from "@lime-bundles/core";
86
+ function renderMixMatchBundle(container, bundle, onAddToCart) {
86
87
  const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
87
88
  const selections = /* @__PURE__ */ new Map();
88
89
  const title = document.createElement("h3");
@@ -146,29 +147,31 @@ function renderMixMatchBundle(container, bundle, addToCart) {
146
147
  button.disabled = true;
147
148
  container.appendChild(button);
148
149
  function updateCta() {
149
- const total = Array.from(selections.values()).reduce((s, v) => s + v.quantity, 0);
150
- const validation = validateQuantity(total, bundle.minQuantity, bundle.maxQuantity);
150
+ const total = Array.from(selections.values()).reduce(
151
+ (s, v) => s + v.quantity,
152
+ 0
153
+ );
154
+ const validation = validateQuantity(
155
+ total,
156
+ bundle.minQuantity,
157
+ bundle.maxQuantity
158
+ );
151
159
  button.disabled = !validation.valid;
152
160
  button.textContent = bundle.widgetConfig.ctaText ?? `Add ${total} Items to Cart`;
153
161
  validationEl.textContent = validation.message ?? "";
154
162
  }
155
163
  updateCta();
156
- button.addEventListener("click", async () => {
157
- button.disabled = true;
158
- button.textContent = "Adding...";
159
- const items = Array.from(selections.entries()).map(([, s]) => ({
160
- variantId: s.variantId,
161
- quantity: s.quantity
164
+ button.addEventListener("click", () => {
165
+ const lines = Array.from(selections.values()).map((s) => ({
166
+ merchandiseId: s.variantId,
167
+ quantity: s.quantity,
168
+ attributes: [
169
+ { key: "_lime_bundle_gid", value: bundle.id },
170
+ { key: "_lime_bundle_type", value: bundle.bundleType }
171
+ ]
162
172
  }));
163
- const result = await addToCart(items);
164
- updateCta();
165
- if (!result.success) {
166
- const error = document.createElement("p");
167
- error.className = "lb-bundle__error";
168
- error.textContent = result.error ?? "Failed to add to cart";
169
- container.appendChild(error);
170
- setTimeout(() => error.remove(), 5e3);
171
- }
173
+ if (lines.length === 0) return;
174
+ onAddToCart(lines);
172
175
  });
173
176
  }
174
177
  function escapeHtml2(str) {
@@ -178,8 +181,11 @@ function escapeHtml2(str) {
178
181
  }
179
182
 
180
183
  // src/renderers/volume.ts
181
- import { formatMoney as formatMoney3, calculateTierSavings } from "@lime-bundles/core";
182
- function renderVolumeBundle(container, bundle, addToCart) {
184
+ import {
185
+ formatMoney as formatMoney3,
186
+ calculateTierSavings
187
+ } from "@lime-bundles/core";
188
+ function renderVolumeBundle(container, bundle, onAddToCart) {
183
189
  const product = bundle.products[0];
184
190
  if (!product) return;
185
191
  const basePrice = parseFloat(product.priceRange.minVariantPrice.amount);
@@ -239,7 +245,11 @@ function renderVolumeBundle(container, bundle, addToCart) {
239
245
  button.setAttribute("part", "button");
240
246
  container.appendChild(button);
241
247
  function updateTiers() {
242
- const savings = calculateTierSavings(bundle.volumeTiers, basePrice, quantity);
248
+ const savings = calculateTierSavings(
249
+ bundle.volumeTiers,
250
+ basePrice,
251
+ quantity
252
+ );
243
253
  tiersDiv.innerHTML = "";
244
254
  for (const ts of savings) {
245
255
  const row = document.createElement("div");
@@ -275,22 +285,19 @@ function renderVolumeBundle(container, bundle, addToCart) {
275
285
  updateTiers();
276
286
  }
277
287
  });
278
- button.addEventListener("click", async () => {
288
+ button.addEventListener("click", () => {
279
289
  const variant = product.variants.nodes.find((v) => v.availableForSale);
280
290
  if (!variant) return;
281
- button.disabled = true;
282
- button.textContent = "Adding...";
283
- const items = [{ variantId: variant.id, quantity }];
284
- const result = await addToCart(items);
285
- button.disabled = false;
286
- updateTiers();
287
- if (!result.success) {
288
- const error = document.createElement("p");
289
- error.className = "lb-bundle__error";
290
- error.textContent = result.error ?? "Failed to add to cart";
291
- container.appendChild(error);
292
- setTimeout(() => error.remove(), 5e3);
293
- }
291
+ onAddToCart([
292
+ {
293
+ merchandiseId: variant.id,
294
+ quantity,
295
+ attributes: [
296
+ { key: "_lime_bundle_gid", value: bundle.id },
297
+ { key: "_lime_bundle_type", value: bundle.bundleType }
298
+ ]
299
+ }
300
+ ]);
294
301
  });
295
302
  }
296
303
  function escapeHtml3(str) {
@@ -371,7 +378,6 @@ var LimeBundleElement = class extends HTMLElement {
371
378
  "shop-domain",
372
379
  "storefront-token",
373
380
  "bundle-gid",
374
- "cart-id",
375
381
  "app-url",
376
382
  "analytics",
377
383
  "locale"
@@ -392,7 +398,6 @@ var LimeBundleElement = class extends HTMLElement {
392
398
  this.abortController?.abort();
393
399
  this.teardownImpression();
394
400
  }
395
- /** Clean up stale bundle state and active impression observer. */
396
401
  teardownImpression() {
397
402
  this.impressionCleanup?.();
398
403
  this.impressionCleanup = null;
@@ -414,9 +419,6 @@ var LimeBundleElement = class extends HTMLElement {
414
419
  get bundleGid() {
415
420
  return this.getAttribute("bundle-gid") ?? "";
416
421
  }
417
- get cartId() {
418
- return this.getAttribute("cart-id") ?? void 0;
419
- }
420
422
  get appUrl() {
421
423
  return this.getAttribute("app-url") ?? "";
422
424
  }
@@ -425,7 +427,9 @@ var LimeBundleElement = class extends HTMLElement {
425
427
  }
426
428
  async fetchBundle() {
427
429
  if (!this.shopDomain || !this.storefrontToken || !this.bundleGid) {
428
- this.renderError("Missing required attributes: shop-domain, storefront-token, bundle-gid");
430
+ this.renderError(
431
+ "Missing required attributes: shop-domain, storefront-token, bundle-gid"
432
+ );
429
433
  return;
430
434
  }
431
435
  this.abortController?.abort();
@@ -437,26 +441,37 @@ var LimeBundleElement = class extends HTMLElement {
437
441
  shopDomain: this.shopDomain,
438
442
  accessToken: this.storefrontToken
439
443
  });
440
- const data = await client.query(
441
- BUNDLE_METAOBJECT_QUERY,
442
- { id: this.bundleGid }
443
- );
444
+ const [bundleData, cssData] = await Promise.all([
445
+ client.query(
446
+ BUNDLE_METAOBJECT_QUERY,
447
+ { id: this.bundleGid },
448
+ { signal: controller.signal }
449
+ ),
450
+ client.query(
451
+ SHOP_CUSTOM_CSS_QUERY,
452
+ void 0,
453
+ { signal: controller.signal }
454
+ ).catch(() => null)
455
+ ]);
444
456
  if (controller.signal.aborted) return;
445
- if (!data.metaobject) {
457
+ if (!bundleData.metaobject) {
446
458
  this.bundle = null;
447
459
  this.teardownImpression();
448
460
  this.renderError("Bundle not found");
449
461
  return;
450
462
  }
451
463
  this.bundle = parseMetaobjectBundle(
452
- data.metaobject.id,
453
- data.metaobject.fields
464
+ bundleData.metaobject.id,
465
+ bundleData.metaobject.fields
454
466
  );
455
467
  if (!this.bundle) {
456
468
  this.teardownImpression();
457
469
  this.renderError("Bundle is not active or has expired");
458
470
  return;
459
471
  }
472
+ if (cssData?.shop?.metafield?.value) {
473
+ injectCustomCss(this.shopDomain, cssData.shop.metafield.value);
474
+ }
460
475
  this.renderBundle();
461
476
  this.setupImpression();
462
477
  } catch (err) {
@@ -468,73 +483,70 @@ var LimeBundleElement = class extends HTMLElement {
468
483
  );
469
484
  }
470
485
  }
486
+ /**
487
+ * Dispatch add-to-cart for merchant handling. Returns true — the widget
488
+ * reports success optimistically. If the merchant's cart mutation fails,
489
+ * they're responsible for surfacing that error in their own UI.
490
+ */
491
+ dispatchAddToCart = (lines) => {
492
+ this.dispatchEvent(
493
+ new CustomEvent("lime-bundle:add-to-cart", {
494
+ detail: { lines },
495
+ bubbles: true,
496
+ composed: true
497
+ })
498
+ );
499
+ if (this.analyticsEnabled && this.appUrl && this.bundle) {
500
+ const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
501
+ const totalPrice = lines.reduce((sum, line) => {
502
+ const product = this.bundle.products.find(
503
+ (p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
504
+ );
505
+ const variant = product?.variants.nodes.find(
506
+ (v) => v.id === line.merchandiseId
507
+ );
508
+ const price = variant ? parseFloat(variant.price.amount) : 0;
509
+ return sum + price * line.quantity;
510
+ }, 0);
511
+ reportAddToCart(
512
+ { shopDomain: this.shopDomain, appUrl: this.appUrl },
513
+ {
514
+ bundleGid: this.bundleGid,
515
+ bundleType: this.bundle.bundleType,
516
+ productId: this.bundle.products[0]?.id ?? "",
517
+ quantity,
518
+ totalPrice: Math.round(totalPrice * 100) / 100
519
+ }
520
+ );
521
+ }
522
+ };
471
523
  renderBundle() {
472
524
  if (!this.bundle) return;
473
525
  const container = document.createElement("div");
474
526
  container.className = "lb-bundle";
475
527
  container.setAttribute("role", "region");
476
528
  container.setAttribute("aria-label", this.bundle.title);
477
- const addToCart = async (items) => {
478
- const apiType = detectCartApi();
479
- const cart = apiType === "ajax" ? createAjaxCartApi(this.bundleGid, this.bundle.bundleType) : createStorefrontCartApi(
480
- createStorefrontClient({
481
- shopDomain: this.shopDomain,
482
- accessToken: this.storefrontToken
483
- }),
484
- this.bundleGid,
485
- this.bundle.bundleType,
486
- this.cartId
487
- );
488
- let result;
489
- try {
490
- result = await cart.addLines(items);
491
- } catch (err) {
492
- result = {
493
- success: false,
494
- error: err instanceof Error ? err.message : "Cart add failed"
495
- };
496
- }
497
- if (result.success) {
498
- this.dispatchEvent(
499
- new CustomEvent("lime-bundle:add-to-cart", {
500
- detail: { items, cartId: result.cartId },
501
- bubbles: true,
502
- composed: true
503
- })
504
- );
505
- if (this.analyticsEnabled && this.appUrl) {
506
- const quantity = items.reduce((sum, i) => sum + i.quantity, 0);
507
- const totalPrice = items.reduce((sum, item) => {
508
- const product = this.bundle.products.find(
509
- (p) => p.variants.nodes.some((v) => v.id === item.variantId)
510
- );
511
- const variant = product?.variants.nodes.find((v) => v.id === item.variantId);
512
- const price = variant ? parseFloat(variant.price.amount) : 0;
513
- return sum + price * item.quantity;
514
- }, 0);
515
- reportAddToCart(
516
- { shopDomain: this.shopDomain, appUrl: this.appUrl },
517
- {
518
- bundleGid: this.bundleGid,
519
- bundleType: this.bundle.bundleType,
520
- productId: this.bundle.products[0]?.id ?? "",
521
- quantity,
522
- totalPrice: Math.round(totalPrice * 100) / 100
523
- }
524
- );
525
- }
526
- }
527
- return result;
528
- };
529
529
  switch (this.bundle.bundleType) {
530
530
  case "fixed":
531
- renderFixedBundle(container, this.bundle, addToCart);
531
+ renderFixedBundle(
532
+ container,
533
+ this.bundle,
534
+ this.dispatchAddToCart
535
+ );
532
536
  break;
533
537
  case "mix_match":
534
- renderMixMatchBundle(container, this.bundle, addToCart);
538
+ renderMixMatchBundle(
539
+ container,
540
+ this.bundle,
541
+ this.dispatchAddToCart
542
+ );
535
543
  break;
536
544
  case "volume":
537
- renderVolumeBundle(container, this.bundle, addToCart);
545
+ renderVolumeBundle(
546
+ container,
547
+ this.bundle,
548
+ this.dispatchAddToCart
549
+ );
538
550
  break;
539
551
  }
540
552
  this.shadow.innerHTML = "";
@@ -590,65 +602,11 @@ var LimeBundleElement = class extends HTMLElement {
590
602
  }
591
603
  };
592
604
 
593
- // src/thankyou/index.ts
594
- var reportedPurchases = /* @__PURE__ */ new Set();
595
- function trackPurchase(input) {
596
- const appUrl = input.appUrl ?? `https://${input.shopDomain}`;
597
- const bundleMap = /* @__PURE__ */ new Map();
598
- for (const item of input.lineItems) {
599
- if (!item.bundleGid) continue;
600
- const existing = bundleMap.get(item.bundleGid);
601
- if (existing) {
602
- existing.revenue += item.price * item.quantity;
603
- existing.lineItemCount += 1;
604
- } else {
605
- bundleMap.set(item.bundleGid, {
606
- bundleType: item.bundleType,
607
- revenue: item.price * item.quantity,
608
- lineItemCount: 1
609
- });
610
- }
611
- }
612
- for (const [bundleGid, data] of bundleMap) {
613
- const dedupKey = `${input.orderId}:${bundleGid}`;
614
- if (reportedPurchases.has(dedupKey)) continue;
615
- reportedPurchases.add(dedupKey);
616
- const payload = {
617
- shopDomain: input.shopDomain,
618
- eventType: "bundle_purchased",
619
- bundleGid,
620
- bundleType: data.bundleType,
621
- orderId: input.orderId,
622
- revenue: Math.round(data.revenue * 100) / 100,
623
- lineItemCount: data.lineItemCount,
624
- occurredAt: (/* @__PURE__ */ new Date()).toISOString()
625
- };
626
- const url = `${appUrl}/api/analytics`;
627
- const body = JSON.stringify(payload);
628
- try {
629
- fetch(url, {
630
- method: "POST",
631
- headers: { "Content-Type": "application/json" },
632
- body
633
- }).catch(() => {
634
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
635
- navigator.sendBeacon(url, body);
636
- }
637
- });
638
- } catch {
639
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
640
- navigator.sendBeacon(url, body);
641
- }
642
- }
643
- }
644
- }
645
-
646
605
  // src/index.ts
647
606
  if (typeof customElements !== "undefined" && !customElements.get("lime-bundle")) {
648
607
  customElements.define("lime-bundle", LimeBundleElement);
649
608
  }
650
609
  export {
651
- LimeBundleElement,
652
- trackPurchase
610
+ LimeBundleElement
653
611
  };
654
612
  //# sourceMappingURL=index.js.map