@lime-bundles/widget 0.1.1 → 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 +291 -214
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -37
- package/dist/index.d.ts +74 -37
- package/dist/index.js +308 -219
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +127 -34
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -6
- package/dist/lime-thankyou.cjs +0 -2
- package/dist/lime-thankyou.cjs.map +0 -1
- package/dist/lime-thankyou.global.js +0 -2
- package/dist/lime-thankyou.global.js.map +0 -1
- package/dist/lime-thankyou.js +0 -2
- package/dist/lime-thankyou.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createStorefrontClient,
|
|
4
4
|
BUNDLE_METAOBJECT_QUERY,
|
|
5
|
+
BUNDLES_FOR_PRODUCT_QUERY,
|
|
6
|
+
CART_CREATE_MUTATION,
|
|
7
|
+
CART_LINES_ADD_MUTATION,
|
|
8
|
+
SHOP_CUSTOM_CSS_QUERY,
|
|
5
9
|
parseMetaobjectBundle,
|
|
6
|
-
detectCartApi,
|
|
7
|
-
createAjaxCartApi,
|
|
8
|
-
createStorefrontCartApi,
|
|
9
10
|
observeImpression,
|
|
10
11
|
reportImpression,
|
|
11
|
-
reportAddToCart
|
|
12
|
+
reportAddToCart,
|
|
13
|
+
injectCustomCss,
|
|
14
|
+
fetchBundlesForProduct
|
|
12
15
|
} from "@lime-bundles/core";
|
|
13
16
|
|
|
14
17
|
// src/renderers/fixed.ts
|
|
15
|
-
import {
|
|
16
|
-
|
|
18
|
+
import {
|
|
19
|
+
formatMoney
|
|
20
|
+
} from "@lime-bundles/core";
|
|
21
|
+
function renderFixedBundle(container, bundle, onAddToCart) {
|
|
17
22
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
18
23
|
const title = document.createElement("h3");
|
|
19
24
|
title.className = "lb-bundle__title";
|
|
@@ -54,23 +59,20 @@ function renderFixedBundle(container, bundle, addToCart) {
|
|
|
54
59
|
button.className = "lb-bundle__cta";
|
|
55
60
|
button.textContent = bundle.widgetConfig.ctaText ?? "Add Bundle to Cart";
|
|
56
61
|
button.setAttribute("part", "button");
|
|
57
|
-
button.addEventListener("click",
|
|
58
|
-
|
|
59
|
-
button.textContent = "Adding...";
|
|
60
|
-
const items = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
|
|
62
|
+
button.addEventListener("click", () => {
|
|
63
|
+
const lines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
|
|
61
64
|
const variant = p.variants.nodes.find((v) => v.availableForSale);
|
|
62
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
merchandiseId: variant.id,
|
|
67
|
+
quantity: 1,
|
|
68
|
+
attributes: [
|
|
69
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
70
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
71
|
+
]
|
|
72
|
+
};
|
|
63
73
|
});
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|
|
74
|
+
if (lines.length === 0) return;
|
|
75
|
+
onAddToCart(lines);
|
|
74
76
|
});
|
|
75
77
|
container.appendChild(button);
|
|
76
78
|
}
|
|
@@ -81,8 +83,11 @@ function escapeHtml(str) {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// src/renderers/mix-match.ts
|
|
84
|
-
import {
|
|
85
|
-
|
|
86
|
+
import {
|
|
87
|
+
formatMoney as formatMoney2,
|
|
88
|
+
validateQuantity
|
|
89
|
+
} from "@lime-bundles/core";
|
|
90
|
+
function renderMixMatchBundle(container, bundle, onAddToCart) {
|
|
86
91
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
87
92
|
const selections = /* @__PURE__ */ new Map();
|
|
88
93
|
const title = document.createElement("h3");
|
|
@@ -146,29 +151,31 @@ function renderMixMatchBundle(container, bundle, addToCart) {
|
|
|
146
151
|
button.disabled = true;
|
|
147
152
|
container.appendChild(button);
|
|
148
153
|
function updateCta() {
|
|
149
|
-
const total = Array.from(selections.values()).reduce(
|
|
150
|
-
|
|
154
|
+
const total = Array.from(selections.values()).reduce(
|
|
155
|
+
(s, v) => s + v.quantity,
|
|
156
|
+
0
|
|
157
|
+
);
|
|
158
|
+
const validation = validateQuantity(
|
|
159
|
+
total,
|
|
160
|
+
bundle.minQuantity,
|
|
161
|
+
bundle.maxQuantity
|
|
162
|
+
);
|
|
151
163
|
button.disabled = !validation.valid;
|
|
152
164
|
button.textContent = bundle.widgetConfig.ctaText ?? `Add ${total} Items to Cart`;
|
|
153
165
|
validationEl.textContent = validation.message ?? "";
|
|
154
166
|
}
|
|
155
167
|
updateCta();
|
|
156
|
-
button.addEventListener("click",
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
button.addEventListener("click", () => {
|
|
169
|
+
const lines = Array.from(selections.values()).map((s) => ({
|
|
170
|
+
merchandiseId: s.variantId,
|
|
171
|
+
quantity: s.quantity,
|
|
172
|
+
attributes: [
|
|
173
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
174
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
175
|
+
]
|
|
162
176
|
}));
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
}
|
|
177
|
+
if (lines.length === 0) return;
|
|
178
|
+
onAddToCart(lines);
|
|
172
179
|
});
|
|
173
180
|
}
|
|
174
181
|
function escapeHtml2(str) {
|
|
@@ -178,8 +185,11 @@ function escapeHtml2(str) {
|
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
// src/renderers/volume.ts
|
|
181
|
-
import {
|
|
182
|
-
|
|
188
|
+
import {
|
|
189
|
+
formatMoney as formatMoney3,
|
|
190
|
+
calculateTierSavings
|
|
191
|
+
} from "@lime-bundles/core";
|
|
192
|
+
function renderVolumeBundle(container, bundle, onAddToCart) {
|
|
183
193
|
const product = bundle.products[0];
|
|
184
194
|
if (!product) return;
|
|
185
195
|
const basePrice = parseFloat(product.priceRange.minVariantPrice.amount);
|
|
@@ -239,7 +249,11 @@ function renderVolumeBundle(container, bundle, addToCart) {
|
|
|
239
249
|
button.setAttribute("part", "button");
|
|
240
250
|
container.appendChild(button);
|
|
241
251
|
function updateTiers() {
|
|
242
|
-
const savings = calculateTierSavings(
|
|
252
|
+
const savings = calculateTierSavings(
|
|
253
|
+
bundle.volumeTiers,
|
|
254
|
+
basePrice,
|
|
255
|
+
quantity
|
|
256
|
+
);
|
|
243
257
|
tiersDiv.innerHTML = "";
|
|
244
258
|
for (const ts of savings) {
|
|
245
259
|
const row = document.createElement("div");
|
|
@@ -275,22 +289,19 @@ function renderVolumeBundle(container, bundle, addToCart) {
|
|
|
275
289
|
updateTiers();
|
|
276
290
|
}
|
|
277
291
|
});
|
|
278
|
-
button.addEventListener("click",
|
|
292
|
+
button.addEventListener("click", () => {
|
|
279
293
|
const variant = product.variants.nodes.find((v) => v.availableForSale);
|
|
280
294
|
if (!variant) return;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
container.appendChild(error);
|
|
292
|
-
setTimeout(() => error.remove(), 5e3);
|
|
293
|
-
}
|
|
295
|
+
onAddToCart([
|
|
296
|
+
{
|
|
297
|
+
merchandiseId: variant.id,
|
|
298
|
+
quantity,
|
|
299
|
+
attributes: [
|
|
300
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
301
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
]);
|
|
294
305
|
});
|
|
295
306
|
}
|
|
296
307
|
function escapeHtml3(str) {
|
|
@@ -366,20 +377,35 @@ var WIDGET_STYLES = `
|
|
|
366
377
|
`;
|
|
367
378
|
|
|
368
379
|
// src/lime-bundle.ts
|
|
380
|
+
var cartStorageKey = (shopDomain) => `lb_cart_id:${shopDomain}`;
|
|
381
|
+
function resolveProductHandle(explicit) {
|
|
382
|
+
if (explicit) return explicit.trim() || null;
|
|
383
|
+
if (typeof document !== "undefined") {
|
|
384
|
+
const meta = document.querySelector(
|
|
385
|
+
'meta[name="shopify:product-handle"]'
|
|
386
|
+
);
|
|
387
|
+
if (meta?.content) return meta.content.trim() || null;
|
|
388
|
+
}
|
|
389
|
+
if (typeof window !== "undefined") {
|
|
390
|
+
const match = window.location.pathname.match(/\/products\/([^/?#]+)/);
|
|
391
|
+
if (match?.[1]) return decodeURIComponent(match[1]);
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
369
395
|
var LimeBundleElement = class extends HTMLElement {
|
|
370
396
|
static observedAttributes = [
|
|
371
397
|
"shop-domain",
|
|
372
398
|
"storefront-token",
|
|
373
399
|
"bundle-gid",
|
|
374
|
-
"
|
|
400
|
+
"product-handle",
|
|
375
401
|
"app-url",
|
|
376
402
|
"analytics",
|
|
377
403
|
"locale"
|
|
378
404
|
];
|
|
379
405
|
shadow;
|
|
380
|
-
|
|
406
|
+
bundles = [];
|
|
381
407
|
abortController = null;
|
|
382
|
-
|
|
408
|
+
impressionCleanups = [];
|
|
383
409
|
constructor() {
|
|
384
410
|
super();
|
|
385
411
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
@@ -390,17 +416,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
390
416
|
}
|
|
391
417
|
disconnectedCallback() {
|
|
392
418
|
this.abortController?.abort();
|
|
393
|
-
this.
|
|
419
|
+
this.teardownImpressions();
|
|
394
420
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.
|
|
398
|
-
this.impressionCleanup = null;
|
|
421
|
+
teardownImpressions() {
|
|
422
|
+
for (const cleanup of this.impressionCleanups) cleanup();
|
|
423
|
+
this.impressionCleanups = [];
|
|
399
424
|
}
|
|
400
425
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
401
426
|
if (oldValue === newValue || !this.isConnected) return;
|
|
402
|
-
if (name === "bundle-gid" || name === "shop-domain" || name === "storefront-token") {
|
|
403
|
-
if (this.shopDomain && this.storefrontToken
|
|
427
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
428
|
+
if (this.shopDomain && this.storefrontToken) {
|
|
404
429
|
this.fetchBundle();
|
|
405
430
|
}
|
|
406
431
|
}
|
|
@@ -414,8 +439,8 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
414
439
|
get bundleGid() {
|
|
415
440
|
return this.getAttribute("bundle-gid") ?? "";
|
|
416
441
|
}
|
|
417
|
-
get
|
|
418
|
-
return this.getAttribute("
|
|
442
|
+
get productHandleAttr() {
|
|
443
|
+
return this.getAttribute("product-handle") ?? "";
|
|
419
444
|
}
|
|
420
445
|
get appUrl() {
|
|
421
446
|
return this.getAttribute("app-url") ?? "";
|
|
@@ -424,147 +449,265 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
424
449
|
return this.getAttribute("analytics") !== "false";
|
|
425
450
|
}
|
|
426
451
|
async fetchBundle() {
|
|
427
|
-
if (!this.shopDomain || !this.storefrontToken
|
|
428
|
-
this.renderError(
|
|
452
|
+
if (!this.shopDomain || !this.storefrontToken) {
|
|
453
|
+
this.renderError(
|
|
454
|
+
"Missing required attributes: shop-domain, storefront-token"
|
|
455
|
+
);
|
|
429
456
|
return;
|
|
430
457
|
}
|
|
431
458
|
this.abortController?.abort();
|
|
432
459
|
const controller = new AbortController();
|
|
433
460
|
this.abortController = controller;
|
|
434
461
|
this.renderLoading();
|
|
462
|
+
const client = createStorefrontClient({
|
|
463
|
+
shopDomain: this.shopDomain,
|
|
464
|
+
accessToken: this.storefrontToken
|
|
465
|
+
});
|
|
435
466
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
467
|
+
let bundlePromise;
|
|
468
|
+
let singleBundleMode = false;
|
|
469
|
+
if (this.bundleGid) {
|
|
470
|
+
singleBundleMode = true;
|
|
471
|
+
bundlePromise = this.fetchSingleBundle(client, controller.signal);
|
|
472
|
+
} else {
|
|
473
|
+
const handle = resolveProductHandle(this.productHandleAttr);
|
|
474
|
+
if (!handle) {
|
|
475
|
+
this.teardownImpressions();
|
|
476
|
+
this.renderError(
|
|
477
|
+
"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>."
|
|
478
|
+
);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
bundlePromise = this.fetchProductBundles(
|
|
482
|
+
client,
|
|
483
|
+
controller.signal,
|
|
484
|
+
handle
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const cssPromise = client.query(SHOP_CUSTOM_CSS_QUERY, void 0, {
|
|
488
|
+
signal: controller.signal
|
|
489
|
+
}).catch(() => null);
|
|
490
|
+
await bundlePromise;
|
|
444
491
|
if (controller.signal.aborted) return;
|
|
445
|
-
if (
|
|
446
|
-
this.
|
|
447
|
-
this.teardownImpression();
|
|
492
|
+
if (singleBundleMode && this.bundles.length === 0) {
|
|
493
|
+
this.teardownImpressions();
|
|
448
494
|
this.renderError("Bundle not found");
|
|
449
495
|
return;
|
|
450
496
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
);
|
|
455
|
-
if (!this.bundle) {
|
|
456
|
-
this.teardownImpression();
|
|
457
|
-
this.renderError("Bundle is not active or has expired");
|
|
458
|
-
return;
|
|
497
|
+
const css = await cssPromise;
|
|
498
|
+
if (css?.shop?.metafield?.value) {
|
|
499
|
+
injectCustomCss(this.shopDomain, css.shop.metafield.value);
|
|
459
500
|
}
|
|
460
|
-
this.
|
|
461
|
-
this.setupImpression();
|
|
501
|
+
this.renderBundles();
|
|
462
502
|
} catch (err) {
|
|
463
503
|
if (controller.signal.aborted) return;
|
|
464
|
-
this.
|
|
465
|
-
this.
|
|
504
|
+
this.bundles = [];
|
|
505
|
+
this.teardownImpressions();
|
|
466
506
|
this.renderError(
|
|
467
507
|
err instanceof Error ? err.message : "Failed to load bundle"
|
|
468
508
|
);
|
|
469
509
|
}
|
|
470
510
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
511
|
+
async fetchSingleBundle(client, signal) {
|
|
512
|
+
const data = await client.query(
|
|
513
|
+
BUNDLE_METAOBJECT_QUERY,
|
|
514
|
+
{ id: this.bundleGid },
|
|
515
|
+
{ signal }
|
|
516
|
+
);
|
|
517
|
+
if (!data.metaobject) {
|
|
518
|
+
this.bundles = [];
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const parsed = parseMetaobjectBundle(
|
|
522
|
+
data.metaobject.id,
|
|
523
|
+
data.metaobject.fields
|
|
524
|
+
);
|
|
525
|
+
this.bundles = parsed ? [parsed] : [];
|
|
526
|
+
}
|
|
527
|
+
async fetchProductBundles(client, signal, productHandle) {
|
|
528
|
+
const data = await client.query(
|
|
529
|
+
BUNDLES_FOR_PRODUCT_QUERY,
|
|
530
|
+
{ handle: productHandle },
|
|
531
|
+
{ signal }
|
|
532
|
+
);
|
|
533
|
+
if (!data.product) {
|
|
534
|
+
this.bundles = [];
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const refs = data.product.metafield?.references?.nodes ?? [];
|
|
538
|
+
const bundles = [];
|
|
539
|
+
for (const ref of refs) {
|
|
540
|
+
const parsed = parseMetaobjectBundle(ref.id, ref.fields);
|
|
541
|
+
if (parsed) bundles.push(parsed);
|
|
542
|
+
}
|
|
543
|
+
this.bundles = bundles;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Dispatch add-to-cart with a cancelable event, then — unless a listener
|
|
547
|
+
* called preventDefault — execute the default cart-and-checkout flow.
|
|
548
|
+
*
|
|
549
|
+
* `fire-and-forget` against `reportAddToCart` runs regardless so merchants
|
|
550
|
+
* with BYO cart still get analytics.
|
|
551
|
+
*/
|
|
552
|
+
handleAddToCart = async (bundle, lines) => {
|
|
553
|
+
const ev = new CustomEvent("lime-bundle:add-to-cart", {
|
|
554
|
+
detail: { lines },
|
|
555
|
+
bubbles: true,
|
|
556
|
+
composed: true,
|
|
557
|
+
cancelable: true
|
|
558
|
+
});
|
|
559
|
+
const allowDefault = this.dispatchEvent(ev);
|
|
560
|
+
this.reportAddToCartEvent(bundle, lines);
|
|
561
|
+
if (allowDefault) {
|
|
562
|
+
await this.defaultAddToCart(lines);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Default cart flow: Shopify's Storefront Cart API is tokenless, so we
|
|
567
|
+
* don't need any additional scopes. Persist the cart ID in localStorage
|
|
568
|
+
* so subsequent adds on the same browser session join the existing cart
|
|
569
|
+
* instead of creating a new one every click.
|
|
570
|
+
*/
|
|
571
|
+
async defaultAddToCart(lines) {
|
|
572
|
+
if (typeof window === "undefined") return;
|
|
573
|
+
const client = createStorefrontClient({
|
|
574
|
+
shopDomain: this.shopDomain,
|
|
575
|
+
accessToken: this.storefrontToken
|
|
576
|
+
});
|
|
577
|
+
const storage = window.localStorage;
|
|
578
|
+
const key = cartStorageKey(this.shopDomain);
|
|
579
|
+
const existingCartId = storage?.getItem(key) ?? null;
|
|
580
|
+
try {
|
|
581
|
+
let checkoutUrl = null;
|
|
582
|
+
if (existingCartId) {
|
|
583
|
+
const res = await client.query(
|
|
584
|
+
CART_LINES_ADD_MUTATION,
|
|
585
|
+
{ cartId: existingCartId, lines }
|
|
586
|
+
);
|
|
587
|
+
const payload = res.cartLinesAdd;
|
|
588
|
+
if (payload?.userErrors?.length) {
|
|
589
|
+
storage?.removeItem(key);
|
|
590
|
+
} else if (payload?.cart) {
|
|
591
|
+
checkoutUrl = payload.cart.checkoutUrl;
|
|
592
|
+
}
|
|
496
593
|
}
|
|
497
|
-
if (
|
|
594
|
+
if (!checkoutUrl) {
|
|
595
|
+
const res = await client.query(
|
|
596
|
+
CART_CREATE_MUTATION,
|
|
597
|
+
{ input: { lines } }
|
|
598
|
+
);
|
|
599
|
+
const payload = res.cartCreate;
|
|
600
|
+
if (payload?.cart) {
|
|
601
|
+
storage?.setItem(key, payload.cart.id);
|
|
602
|
+
checkoutUrl = payload.cart.checkoutUrl;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (checkoutUrl) {
|
|
606
|
+
window.location.assign(checkoutUrl);
|
|
607
|
+
} else {
|
|
498
608
|
this.dispatchEvent(
|
|
499
|
-
new CustomEvent("lime-bundle:
|
|
500
|
-
detail: {
|
|
609
|
+
new CustomEvent("lime-bundle:error", {
|
|
610
|
+
detail: { message: "Cart creation failed", code: "CART_ERROR" },
|
|
501
611
|
bubbles: true,
|
|
502
612
|
composed: true
|
|
503
613
|
})
|
|
504
614
|
);
|
|
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
615
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
break;
|
|
616
|
+
} catch (err) {
|
|
617
|
+
this.dispatchEvent(
|
|
618
|
+
new CustomEvent("lime-bundle:error", {
|
|
619
|
+
detail: {
|
|
620
|
+
message: err instanceof Error ? err.message : "Cart mutation failed",
|
|
621
|
+
code: "CART_ERROR"
|
|
622
|
+
},
|
|
623
|
+
bubbles: true,
|
|
624
|
+
composed: true
|
|
625
|
+
})
|
|
626
|
+
);
|
|
539
627
|
}
|
|
628
|
+
}
|
|
629
|
+
reportAddToCartEvent(bundle, lines) {
|
|
630
|
+
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
631
|
+
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
632
|
+
const totalPrice = lines.reduce((sum, line) => {
|
|
633
|
+
const product = bundle.products.find(
|
|
634
|
+
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
635
|
+
);
|
|
636
|
+
const variant = product?.variants.nodes.find(
|
|
637
|
+
(v) => v.id === line.merchandiseId
|
|
638
|
+
);
|
|
639
|
+
const price = variant ? parseFloat(variant.price.amount) : 0;
|
|
640
|
+
return sum + price * line.quantity;
|
|
641
|
+
}, 0);
|
|
642
|
+
reportAddToCart(
|
|
643
|
+
{ shopDomain: this.shopDomain, appUrl: this.appUrl },
|
|
644
|
+
{
|
|
645
|
+
bundleGid: bundle.id,
|
|
646
|
+
bundleType: bundle.bundleType,
|
|
647
|
+
productId: bundle.products[0]?.id ?? "",
|
|
648
|
+
quantity,
|
|
649
|
+
totalPrice: Math.round(totalPrice * 100) / 100
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
renderBundles() {
|
|
654
|
+
this.teardownImpressions();
|
|
540
655
|
this.shadow.innerHTML = "";
|
|
541
656
|
const style = document.createElement("style");
|
|
542
657
|
style.textContent = WIDGET_STYLES;
|
|
543
658
|
this.shadow.appendChild(style);
|
|
544
|
-
this.
|
|
659
|
+
for (const bundle of this.bundles) {
|
|
660
|
+
const container = document.createElement("div");
|
|
661
|
+
container.className = "lb-bundle";
|
|
662
|
+
container.setAttribute("role", "region");
|
|
663
|
+
container.setAttribute("aria-label", bundle.title);
|
|
664
|
+
const dispatch = (lines) => this.handleAddToCart(bundle, lines);
|
|
665
|
+
switch (bundle.bundleType) {
|
|
666
|
+
case "fixed":
|
|
667
|
+
renderFixedBundle(container, bundle, dispatch);
|
|
668
|
+
break;
|
|
669
|
+
case "mix_match":
|
|
670
|
+
renderMixMatchBundle(
|
|
671
|
+
container,
|
|
672
|
+
bundle,
|
|
673
|
+
dispatch
|
|
674
|
+
);
|
|
675
|
+
break;
|
|
676
|
+
case "volume":
|
|
677
|
+
renderVolumeBundle(container, bundle, dispatch);
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
this.shadow.appendChild(container);
|
|
681
|
+
this.setupImpressionFor(bundle, container);
|
|
682
|
+
}
|
|
683
|
+
const first = this.bundles[0];
|
|
545
684
|
this.dispatchEvent(
|
|
546
685
|
new CustomEvent("lime-bundle:loaded", {
|
|
547
686
|
detail: {
|
|
548
|
-
|
|
549
|
-
|
|
687
|
+
bundleCount: this.bundles.length,
|
|
688
|
+
bundleTypes: this.bundles.map((b) => b.bundleType),
|
|
689
|
+
// Legacy fields — meaningful only in single-bundle mode. Preserved
|
|
690
|
+
// for merchants who attached listeners against the pre-1.0 shape.
|
|
691
|
+
bundleType: first?.bundleType,
|
|
692
|
+
title: first?.title
|
|
550
693
|
},
|
|
551
694
|
bubbles: true,
|
|
552
695
|
composed: true
|
|
553
696
|
})
|
|
554
697
|
);
|
|
555
698
|
}
|
|
556
|
-
|
|
557
|
-
if (!this.analyticsEnabled || !this.
|
|
558
|
-
|
|
559
|
-
this.impressionCleanup = observeImpression(this, () => {
|
|
699
|
+
setupImpressionFor(bundle, element) {
|
|
700
|
+
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
701
|
+
const cleanup = observeImpression(element, () => {
|
|
560
702
|
reportImpression(
|
|
561
703
|
{ shopDomain: this.shopDomain, appUrl: this.appUrl },
|
|
562
704
|
{
|
|
563
|
-
bundleGid:
|
|
564
|
-
bundleType:
|
|
705
|
+
bundleGid: bundle.id,
|
|
706
|
+
bundleType: bundle.bundleType
|
|
565
707
|
}
|
|
566
708
|
);
|
|
567
709
|
});
|
|
710
|
+
this.impressionCleanups.push(cleanup);
|
|
568
711
|
}
|
|
569
712
|
renderLoading() {
|
|
570
713
|
this.shadow.innerHTML = `
|
|
@@ -590,65 +733,11 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
590
733
|
}
|
|
591
734
|
};
|
|
592
735
|
|
|
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
736
|
// src/index.ts
|
|
647
737
|
if (typeof customElements !== "undefined" && !customElements.get("lime-bundle")) {
|
|
648
738
|
customElements.define("lime-bundle", LimeBundleElement);
|
|
649
739
|
}
|
|
650
740
|
export {
|
|
651
|
-
LimeBundleElement
|
|
652
|
-
trackPurchase
|
|
741
|
+
LimeBundleElement
|
|
653
742
|
};
|
|
654
743
|
//# sourceMappingURL=index.js.map
|