@lime-bundles/widget 0.2.0 → 1.0.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 +155 -0
- package/dist/index.cjs +238 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -9
- package/dist/index.d.ts +71 -9
- package/dist/index.js +243 -112
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +126 -17
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# @lime-bundles/widget
|
|
2
|
+
|
|
3
|
+
Framework-agnostic `<lime-bundle>` custom element for rendering Lime Bundles on any storefront: Astro, Vue, Svelte, plain HTML, classic Shopify themes via `<script>` tag.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Via npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @lime-bundles/widget
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import "@lime-bundles/widget"; // registers <lime-bundle> globally
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or via CDN, zero build step:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script type="module" src="https://unpkg.com/@lime-bundles/widget"></script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage: the paste-and-go default
|
|
24
|
+
|
|
25
|
+
One snippet in your product page template. The widget auto-detects the current product from the URL (`/products/<handle>`) and renders every active bundle for it. On "Add bundle", the widget calls Shopify's tokenless Storefront Cart API and redirects to checkout with the bundle discount applied. No cart code required.
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script type="module" src="https://unpkg.com/@lime-bundles/widget"></script>
|
|
29
|
+
<lime-bundle
|
|
30
|
+
shop-domain="my-shop.myshopify.com"
|
|
31
|
+
storefront-token="<YOUR_LIME_BUNDLES_TOKEN>"
|
|
32
|
+
></lime-bundle>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### BYO cart
|
|
36
|
+
|
|
37
|
+
If you have your own cart, listen for `lime-bundle:add-to-cart` and call `event.preventDefault()` to suppress the default redirect:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<script>
|
|
41
|
+
document.querySelector("lime-bundle").addEventListener(
|
|
42
|
+
"lime-bundle:add-to-cart",
|
|
43
|
+
async (event) => {
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
await myCart.linesAdd(event.detail.lines);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Attributes
|
|
52
|
+
|
|
53
|
+
| Attribute | Required | Purpose |
|
|
54
|
+
|---|:-:|---|
|
|
55
|
+
| `shop-domain` | ✓ | Your shop domain, e.g. `my-shop.myshopify.com`. |
|
|
56
|
+
| `storefront-token` | ✓ | Public Storefront Access Token. Generated in `/app/settings/headless`. |
|
|
57
|
+
| `bundle-gid` | | Pin one specific bundle. When set, overrides auto-detect. |
|
|
58
|
+
| `product-handle` | | Render bundles for a specific product handle. Overrides URL detection. |
|
|
59
|
+
| `app-url` | | Lime Bundles app URL; enables analytics when set. |
|
|
60
|
+
| `analytics` | | Set to `"false"` to suppress analytics even with `app-url` set. |
|
|
61
|
+
| `locale` | | BCP-47 tag forwarded to Storefront API. |
|
|
62
|
+
|
|
63
|
+
Product resolution cascade when `bundle-gid` is absent: explicit `product-handle` → `<meta name="shopify:product-handle">` → `/products/<handle>` URL segment.
|
|
64
|
+
|
|
65
|
+
Changing any attribute at runtime re-fetches and re-renders.
|
|
66
|
+
|
|
67
|
+
## Events
|
|
68
|
+
|
|
69
|
+
### `lime-bundle:add-to-cart`
|
|
70
|
+
|
|
71
|
+
Fired on CTA click. `event.detail`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
{
|
|
75
|
+
lines: CartLineInput[];
|
|
76
|
+
bundleType: "fixed" | "volume" | "mix_match";
|
|
77
|
+
bundleId: string;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Every line's `attributes` array includes `{ key: "_lime_bundle_gid", value: bundleId }`. Preserve it on the way to Shopify cart mutation or purchase attribution breaks.
|
|
82
|
+
|
|
83
|
+
### `lime-bundle:error`
|
|
84
|
+
|
|
85
|
+
Fired if bundle fetch / parse fails. `event.detail.error` is an `Error`. Render your own fallback UI in response.
|
|
86
|
+
|
|
87
|
+
## Framework snippets
|
|
88
|
+
|
|
89
|
+
**Astro:**
|
|
90
|
+
|
|
91
|
+
```astro
|
|
92
|
+
<lime-bundle shop-domain="..." storefront-token={import.meta.env.PUBLIC_LIME_BUNDLES_TOKEN} bundle-gid="..." />
|
|
93
|
+
<script>
|
|
94
|
+
import "@lime-bundles/widget";
|
|
95
|
+
document.querySelector("lime-bundle")!.addEventListener("lime-bundle:add-to-cart", (e: any) => {
|
|
96
|
+
fetch("/api/cart-add", { method: "POST", body: JSON.stringify(e.detail.lines) });
|
|
97
|
+
});
|
|
98
|
+
</script>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Vue 3:**
|
|
102
|
+
|
|
103
|
+
```vue
|
|
104
|
+
<lime-bundle
|
|
105
|
+
shop-domain="my-shop.myshopify.com"
|
|
106
|
+
:storefront-token="token"
|
|
107
|
+
bundle-gid="..."
|
|
108
|
+
@lime-bundle:add-to-cart="handle"
|
|
109
|
+
/>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Configure `app.config.compilerOptions.isCustomElement = (tag) => tag === "lime-bundle"` to silence Vue's warning.
|
|
113
|
+
|
|
114
|
+
**Svelte:**
|
|
115
|
+
|
|
116
|
+
```svelte
|
|
117
|
+
<lime-bundle
|
|
118
|
+
shop-domain="my-shop.myshopify.com"
|
|
119
|
+
storefront-token={TOKEN}
|
|
120
|
+
bundle-gid="..."
|
|
121
|
+
on:lime-bundle:add-to-cart={handle}
|
|
122
|
+
/>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Styling
|
|
126
|
+
|
|
127
|
+
Render happens inside a closed Shadow DOM. Override CSS custom properties on the host:
|
|
128
|
+
|
|
129
|
+
```html
|
|
130
|
+
<lime-bundle
|
|
131
|
+
style="--lb-primary-color: #e91e63; --lb-radius: 16px;"
|
|
132
|
+
shop-domain="..."
|
|
133
|
+
></lime-bundle>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Merchant custom CSS from `/app/settings/custom-css` is auto-fetched and injected on mount.
|
|
137
|
+
|
|
138
|
+
Full variable list: [css-variables.md](https://github.com/lime-app-dev/lime-bundles-app/blob/main/docs/headless/css-variables.md).
|
|
139
|
+
|
|
140
|
+
## Bundle size
|
|
141
|
+
|
|
142
|
+
`lime-bundle.js` (IIFE, gzipped) is <30 KB. No runtime framework dependency.
|
|
143
|
+
|
|
144
|
+
## Versioning
|
|
145
|
+
|
|
146
|
+
Major versions bump together with `@lime-bundles/core` and `@lime-bundles/react`.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT. See repo root.
|
|
151
|
+
|
|
152
|
+
## Links
|
|
153
|
+
|
|
154
|
+
- [Web component guide](https://github.com/lime-app-dev/lime-bundles-app/blob/main/docs/headless/web-component.md)
|
|
155
|
+
- [Report issues](https://github.com/lime-app-dev/lime-bundles-app/issues)
|
package/dist/index.cjs
CHANGED
|
@@ -382,19 +382,35 @@ var WIDGET_STYLES = `
|
|
|
382
382
|
`;
|
|
383
383
|
|
|
384
384
|
// src/lime-bundle.ts
|
|
385
|
+
var cartStorageKey = (shopDomain) => `lb_cart_id:${shopDomain}`;
|
|
386
|
+
function resolveProductHandle(explicit) {
|
|
387
|
+
if (explicit) return explicit.trim() || null;
|
|
388
|
+
if (typeof document !== "undefined") {
|
|
389
|
+
const meta = document.querySelector(
|
|
390
|
+
'meta[name="shopify:product-handle"]'
|
|
391
|
+
);
|
|
392
|
+
if (meta?.content) return meta.content.trim() || null;
|
|
393
|
+
}
|
|
394
|
+
if (typeof window !== "undefined") {
|
|
395
|
+
const match = window.location.pathname.match(/\/products\/([^/?#]+)/);
|
|
396
|
+
if (match?.[1]) return decodeURIComponent(match[1]);
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
385
400
|
var LimeBundleElement = class extends HTMLElement {
|
|
386
401
|
static observedAttributes = [
|
|
387
402
|
"shop-domain",
|
|
388
403
|
"storefront-token",
|
|
389
404
|
"bundle-gid",
|
|
405
|
+
"product-handle",
|
|
390
406
|
"app-url",
|
|
391
407
|
"analytics",
|
|
392
408
|
"locale"
|
|
393
409
|
];
|
|
394
410
|
shadow;
|
|
395
|
-
|
|
411
|
+
bundles = [];
|
|
396
412
|
abortController = null;
|
|
397
|
-
|
|
413
|
+
impressionCleanups = [];
|
|
398
414
|
constructor() {
|
|
399
415
|
super();
|
|
400
416
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
@@ -405,16 +421,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
405
421
|
}
|
|
406
422
|
disconnectedCallback() {
|
|
407
423
|
this.abortController?.abort();
|
|
408
|
-
this.
|
|
424
|
+
this.teardownImpressions();
|
|
409
425
|
}
|
|
410
|
-
|
|
411
|
-
this.
|
|
412
|
-
this.
|
|
426
|
+
teardownImpressions() {
|
|
427
|
+
for (const cleanup of this.impressionCleanups) cleanup();
|
|
428
|
+
this.impressionCleanups = [];
|
|
413
429
|
}
|
|
414
430
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
415
431
|
if (oldValue === newValue || !this.isConnected) return;
|
|
416
|
-
if (name === "bundle-gid" || name === "shop-domain" || name === "storefront-token") {
|
|
417
|
-
if (this.shopDomain && this.storefrontToken
|
|
432
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
433
|
+
if (this.shopDomain && this.storefrontToken) {
|
|
418
434
|
this.fetchBundle();
|
|
419
435
|
}
|
|
420
436
|
}
|
|
@@ -428,6 +444,9 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
428
444
|
get bundleGid() {
|
|
429
445
|
return this.getAttribute("bundle-gid") ?? "";
|
|
430
446
|
}
|
|
447
|
+
get productHandleAttr() {
|
|
448
|
+
return this.getAttribute("product-handle") ?? "";
|
|
449
|
+
}
|
|
431
450
|
get appUrl() {
|
|
432
451
|
return this.getAttribute("app-url") ?? "";
|
|
433
452
|
}
|
|
@@ -435,9 +454,9 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
435
454
|
return this.getAttribute("analytics") !== "false";
|
|
436
455
|
}
|
|
437
456
|
async fetchBundle() {
|
|
438
|
-
if (!this.shopDomain || !this.storefrontToken
|
|
457
|
+
if (!this.shopDomain || !this.storefrontToken) {
|
|
439
458
|
this.renderError(
|
|
440
|
-
"Missing required attributes: shop-domain, storefront-token
|
|
459
|
+
"Missing required attributes: shop-domain, storefront-token"
|
|
441
460
|
);
|
|
442
461
|
return;
|
|
443
462
|
}
|
|
@@ -445,147 +464,255 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
445
464
|
const controller = new AbortController();
|
|
446
465
|
this.abortController = controller;
|
|
447
466
|
this.renderLoading();
|
|
467
|
+
const client = (0, import_core4.createStorefrontClient)({
|
|
468
|
+
shopDomain: this.shopDomain,
|
|
469
|
+
accessToken: this.storefrontToken
|
|
470
|
+
});
|
|
448
471
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
472
|
+
let bundlePromise;
|
|
473
|
+
let singleBundleMode = false;
|
|
474
|
+
if (this.bundleGid) {
|
|
475
|
+
singleBundleMode = true;
|
|
476
|
+
bundlePromise = this.fetchSingleBundle(client, controller.signal);
|
|
477
|
+
} else {
|
|
478
|
+
const handle = resolveProductHandle(this.productHandleAttr);
|
|
479
|
+
if (!handle) {
|
|
480
|
+
this.teardownImpressions();
|
|
481
|
+
this.renderError(
|
|
482
|
+
"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>."
|
|
483
|
+
);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
bundlePromise = this.fetchProductBundles(
|
|
487
|
+
client,
|
|
488
|
+
controller.signal,
|
|
489
|
+
handle
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const cssPromise = client.query(import_core4.SHOP_CUSTOM_CSS_QUERY, void 0, {
|
|
493
|
+
signal: controller.signal
|
|
494
|
+
}).catch(() => null);
|
|
495
|
+
await bundlePromise;
|
|
465
496
|
if (controller.signal.aborted) return;
|
|
466
|
-
if (
|
|
467
|
-
this.
|
|
468
|
-
this.teardownImpression();
|
|
497
|
+
if (singleBundleMode && this.bundles.length === 0) {
|
|
498
|
+
this.teardownImpressions();
|
|
469
499
|
this.renderError("Bundle not found");
|
|
470
500
|
return;
|
|
471
501
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
);
|
|
476
|
-
if (!this.bundle) {
|
|
477
|
-
this.teardownImpression();
|
|
478
|
-
this.renderError("Bundle is not active or has expired");
|
|
479
|
-
return;
|
|
502
|
+
const css = await cssPromise;
|
|
503
|
+
if (css?.shop?.metafield?.value) {
|
|
504
|
+
(0, import_core4.injectCustomCss)(this.shopDomain, css.shop.metafield.value);
|
|
480
505
|
}
|
|
481
|
-
|
|
482
|
-
(0, import_core4.injectCustomCss)(this.shopDomain, cssData.shop.metafield.value);
|
|
483
|
-
}
|
|
484
|
-
this.renderBundle();
|
|
485
|
-
this.setupImpression();
|
|
506
|
+
this.renderBundles();
|
|
486
507
|
} catch (err) {
|
|
487
508
|
if (controller.signal.aborted) return;
|
|
488
|
-
this.
|
|
489
|
-
this.
|
|
509
|
+
this.bundles = [];
|
|
510
|
+
this.teardownImpressions();
|
|
490
511
|
this.renderError(
|
|
491
512
|
err instanceof Error ? err.message : "Failed to load bundle"
|
|
492
513
|
);
|
|
493
514
|
}
|
|
494
515
|
}
|
|
516
|
+
async fetchSingleBundle(client, signal) {
|
|
517
|
+
const data = await client.query(
|
|
518
|
+
import_core4.BUNDLE_METAOBJECT_QUERY,
|
|
519
|
+
{ id: this.bundleGid },
|
|
520
|
+
{ signal }
|
|
521
|
+
);
|
|
522
|
+
if (!data.metaobject) {
|
|
523
|
+
this.bundles = [];
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const parsed = (0, import_core4.parseMetaobjectBundle)(
|
|
527
|
+
data.metaobject.id,
|
|
528
|
+
data.metaobject.fields
|
|
529
|
+
);
|
|
530
|
+
this.bundles = parsed ? [parsed] : [];
|
|
531
|
+
}
|
|
532
|
+
async fetchProductBundles(client, signal, productHandle) {
|
|
533
|
+
const data = await client.query(
|
|
534
|
+
import_core4.BUNDLES_FOR_PRODUCT_QUERY,
|
|
535
|
+
{ handle: productHandle },
|
|
536
|
+
{ signal }
|
|
537
|
+
);
|
|
538
|
+
if (!data.product) {
|
|
539
|
+
this.bundles = [];
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const refs = data.product.metafield?.references?.nodes ?? [];
|
|
543
|
+
const bundles = [];
|
|
544
|
+
for (const ref of refs) {
|
|
545
|
+
const parsed = (0, import_core4.parseMetaobjectBundle)(ref.id, ref.fields);
|
|
546
|
+
if (parsed) bundles.push(parsed);
|
|
547
|
+
}
|
|
548
|
+
this.bundles = bundles;
|
|
549
|
+
}
|
|
495
550
|
/**
|
|
496
|
-
* Dispatch add-to-cart
|
|
497
|
-
*
|
|
498
|
-
*
|
|
551
|
+
* Dispatch add-to-cart with a cancelable event, then — unless a listener
|
|
552
|
+
* called preventDefault — execute the default cart-and-checkout flow.
|
|
553
|
+
*
|
|
554
|
+
* `fire-and-forget` against `reportAddToCart` runs regardless so merchants
|
|
555
|
+
* with BYO cart still get analytics.
|
|
499
556
|
*/
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const product = this.bundle.products.find(
|
|
512
|
-
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
513
|
-
);
|
|
514
|
-
const variant = product?.variants.nodes.find(
|
|
515
|
-
(v) => v.id === line.merchandiseId
|
|
516
|
-
);
|
|
517
|
-
const price = variant ? parseFloat(variant.price.amount) : 0;
|
|
518
|
-
return sum + price * line.quantity;
|
|
519
|
-
}, 0);
|
|
520
|
-
(0, import_core4.reportAddToCart)(
|
|
521
|
-
{ shopDomain: this.shopDomain, appUrl: this.appUrl },
|
|
522
|
-
{
|
|
523
|
-
bundleGid: this.bundleGid,
|
|
524
|
-
bundleType: this.bundle.bundleType,
|
|
525
|
-
productId: this.bundle.products[0]?.id ?? "",
|
|
526
|
-
quantity,
|
|
527
|
-
totalPrice: Math.round(totalPrice * 100) / 100
|
|
528
|
-
}
|
|
529
|
-
);
|
|
557
|
+
handleAddToCart = async (bundle, lines) => {
|
|
558
|
+
const ev = new CustomEvent("lime-bundle:add-to-cart", {
|
|
559
|
+
detail: { lines },
|
|
560
|
+
bubbles: true,
|
|
561
|
+
composed: true,
|
|
562
|
+
cancelable: true
|
|
563
|
+
});
|
|
564
|
+
const allowDefault = this.dispatchEvent(ev);
|
|
565
|
+
this.reportAddToCartEvent(bundle, lines);
|
|
566
|
+
if (allowDefault) {
|
|
567
|
+
await this.defaultAddToCart(lines);
|
|
530
568
|
}
|
|
531
569
|
};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
570
|
+
/**
|
|
571
|
+
* Default cart flow: Shopify's Storefront Cart API is tokenless, so we
|
|
572
|
+
* don't need any additional scopes. Persist the cart ID in localStorage
|
|
573
|
+
* so subsequent adds on the same browser session join the existing cart
|
|
574
|
+
* instead of creating a new one every click.
|
|
575
|
+
*/
|
|
576
|
+
async defaultAddToCart(lines) {
|
|
577
|
+
if (typeof window === "undefined") return;
|
|
578
|
+
const client = (0, import_core4.createStorefrontClient)({
|
|
579
|
+
shopDomain: this.shopDomain,
|
|
580
|
+
accessToken: this.storefrontToken
|
|
581
|
+
});
|
|
582
|
+
const storage = window.localStorage;
|
|
583
|
+
const key = cartStorageKey(this.shopDomain);
|
|
584
|
+
const existingCartId = storage?.getItem(key) ?? null;
|
|
585
|
+
try {
|
|
586
|
+
let checkoutUrl = null;
|
|
587
|
+
if (existingCartId) {
|
|
588
|
+
const res = await client.query(
|
|
589
|
+
import_core4.CART_LINES_ADD_MUTATION,
|
|
590
|
+
{ cartId: existingCartId, lines }
|
|
544
591
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
592
|
+
const payload = res.cartLinesAdd;
|
|
593
|
+
if (payload?.userErrors?.length) {
|
|
594
|
+
storage?.removeItem(key);
|
|
595
|
+
} else if (payload?.cart) {
|
|
596
|
+
checkoutUrl = payload.cart.checkoutUrl;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!checkoutUrl) {
|
|
600
|
+
const res = await client.query(
|
|
601
|
+
import_core4.CART_CREATE_MUTATION,
|
|
602
|
+
{ input: { lines } }
|
|
551
603
|
);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
604
|
+
const payload = res.cartCreate;
|
|
605
|
+
if (payload?.cart) {
|
|
606
|
+
storage?.setItem(key, payload.cart.id);
|
|
607
|
+
checkoutUrl = payload.cart.checkoutUrl;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (checkoutUrl) {
|
|
611
|
+
window.location.assign(checkoutUrl);
|
|
612
|
+
} else {
|
|
613
|
+
this.dispatchEvent(
|
|
614
|
+
new CustomEvent("lime-bundle:error", {
|
|
615
|
+
detail: { message: "Cart creation failed", code: "CART_ERROR" },
|
|
616
|
+
bubbles: true,
|
|
617
|
+
composed: true
|
|
618
|
+
})
|
|
558
619
|
);
|
|
559
|
-
|
|
620
|
+
}
|
|
621
|
+
} catch (err) {
|
|
622
|
+
this.dispatchEvent(
|
|
623
|
+
new CustomEvent("lime-bundle:error", {
|
|
624
|
+
detail: {
|
|
625
|
+
message: err instanceof Error ? err.message : "Cart mutation failed",
|
|
626
|
+
code: "CART_ERROR"
|
|
627
|
+
},
|
|
628
|
+
bubbles: true,
|
|
629
|
+
composed: true
|
|
630
|
+
})
|
|
631
|
+
);
|
|
560
632
|
}
|
|
633
|
+
}
|
|
634
|
+
reportAddToCartEvent(bundle, lines) {
|
|
635
|
+
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
636
|
+
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
637
|
+
const totalPrice = lines.reduce((sum, line) => {
|
|
638
|
+
const product = bundle.products.find(
|
|
639
|
+
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
640
|
+
);
|
|
641
|
+
const variant = product?.variants.nodes.find(
|
|
642
|
+
(v) => v.id === line.merchandiseId
|
|
643
|
+
);
|
|
644
|
+
const price = variant ? parseFloat(variant.price.amount) : 0;
|
|
645
|
+
return sum + price * line.quantity;
|
|
646
|
+
}, 0);
|
|
647
|
+
(0, import_core4.reportAddToCart)(
|
|
648
|
+
{ shopDomain: this.shopDomain, appUrl: this.appUrl },
|
|
649
|
+
{
|
|
650
|
+
bundleGid: bundle.id,
|
|
651
|
+
bundleType: bundle.bundleType,
|
|
652
|
+
productId: bundle.products[0]?.id ?? "",
|
|
653
|
+
quantity,
|
|
654
|
+
totalPrice: Math.round(totalPrice * 100) / 100
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
renderBundles() {
|
|
659
|
+
this.teardownImpressions();
|
|
561
660
|
this.shadow.innerHTML = "";
|
|
562
661
|
const style = document.createElement("style");
|
|
563
662
|
style.textContent = WIDGET_STYLES;
|
|
564
663
|
this.shadow.appendChild(style);
|
|
565
|
-
this.
|
|
664
|
+
for (const bundle of this.bundles) {
|
|
665
|
+
const container = document.createElement("div");
|
|
666
|
+
container.className = "lb-bundle";
|
|
667
|
+
container.setAttribute("role", "region");
|
|
668
|
+
container.setAttribute("aria-label", bundle.title);
|
|
669
|
+
const dispatch = (lines) => this.handleAddToCart(bundle, lines);
|
|
670
|
+
switch (bundle.bundleType) {
|
|
671
|
+
case "fixed":
|
|
672
|
+
renderFixedBundle(container, bundle, dispatch);
|
|
673
|
+
break;
|
|
674
|
+
case "mix_match":
|
|
675
|
+
renderMixMatchBundle(
|
|
676
|
+
container,
|
|
677
|
+
bundle,
|
|
678
|
+
dispatch
|
|
679
|
+
);
|
|
680
|
+
break;
|
|
681
|
+
case "volume":
|
|
682
|
+
renderVolumeBundle(container, bundle, dispatch);
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
this.shadow.appendChild(container);
|
|
686
|
+
this.setupImpressionFor(bundle, container);
|
|
687
|
+
}
|
|
688
|
+
const first = this.bundles[0];
|
|
566
689
|
this.dispatchEvent(
|
|
567
690
|
new CustomEvent("lime-bundle:loaded", {
|
|
568
691
|
detail: {
|
|
569
|
-
|
|
570
|
-
|
|
692
|
+
bundleCount: this.bundles.length,
|
|
693
|
+
bundleTypes: this.bundles.map((b) => b.bundleType),
|
|
694
|
+
// Legacy fields — meaningful only in single-bundle mode. Preserved
|
|
695
|
+
// for merchants who attached listeners against the pre-1.0 shape.
|
|
696
|
+
bundleType: first?.bundleType,
|
|
697
|
+
title: first?.title
|
|
571
698
|
},
|
|
572
699
|
bubbles: true,
|
|
573
700
|
composed: true
|
|
574
701
|
})
|
|
575
702
|
);
|
|
576
703
|
}
|
|
577
|
-
|
|
578
|
-
if (!this.analyticsEnabled || !this.
|
|
579
|
-
|
|
580
|
-
this.impressionCleanup = (0, import_core4.observeImpression)(this, () => {
|
|
704
|
+
setupImpressionFor(bundle, element) {
|
|
705
|
+
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
706
|
+
const cleanup = (0, import_core4.observeImpression)(element, () => {
|
|
581
707
|
(0, import_core4.reportImpression)(
|
|
582
708
|
{ shopDomain: this.shopDomain, appUrl: this.appUrl },
|
|
583
709
|
{
|
|
584
|
-
bundleGid:
|
|
585
|
-
bundleType:
|
|
710
|
+
bundleGid: bundle.id,
|
|
711
|
+
bundleType: bundle.bundleType
|
|
586
712
|
}
|
|
587
713
|
);
|
|
588
714
|
});
|
|
715
|
+
this.impressionCleanups.push(cleanup);
|
|
589
716
|
}
|
|
590
717
|
renderLoading() {
|
|
591
718
|
this.shadow.innerHTML = `
|