@lime-bundles/widget 4.2.0 → 4.3.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 +3 -0
- package/dist/index.cjs +86 -29
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +89 -29
- package/dist/lime-bundle.global.js +38 -18
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ document.querySelector("lime-bundle").addEventListener(
|
|
|
58
58
|
| `storefront-token` | ✓ | Storefront Access Token from `/app/settings/headless`. |
|
|
59
59
|
| `bundle-gid` | | Render one specific bundle. Overrides auto-detect. |
|
|
60
60
|
| `product-handle` | | Render bundles for this handle. Overrides URL detection. |
|
|
61
|
+
| `product-id` | | The product this embed is standing on — full GID or bare numeric id. Volume bundles price and sell this product; falls back to the handle cascade, then the bundle's first product. |
|
|
61
62
|
| `app-url` | | Enables impression + add-to-cart analytics when set. |
|
|
62
63
|
| `analytics` | | `"false"` suppresses analytics even when `app-url` is set. |
|
|
63
64
|
| `locale` | | BCP-47 tag forwarded to the Storefront API. |
|
|
@@ -67,6 +68,8 @@ document.querySelector("lime-bundle").addEventListener(
|
|
|
67
68
|
|
|
68
69
|
**Product resolution** (when `bundle-gid` is absent): explicit `product-handle` → `<meta name="shopify:product-handle">` → `/products/<handle>` URL segment.
|
|
69
70
|
|
|
71
|
+
**Multi-product binding**: a volume bundle can span several products, and the widget prices and sells the one the shopper is looking at: explicit `product-id` → the handle cascade above → the bundle's first product. On a pinned `bundle-gid` embed outside a `/products/<handle>` URL, set `product-id` (or `product-handle`) or the first product is what gets sold.
|
|
72
|
+
|
|
70
73
|
Changing any attribute at runtime re-fetches and re-renders.
|
|
71
74
|
|
|
72
75
|
### B2B buyer identity (programmatic property only)
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,28 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
// src/lime-bundle.ts
|
|
29
29
|
var import_core6 = require("@lime-bundles/core");
|
|
30
30
|
|
|
31
|
+
// ../render/src/host.ts
|
|
32
|
+
function intlFormatMoney(currencyCode) {
|
|
33
|
+
return (cents) => {
|
|
34
|
+
try {
|
|
35
|
+
return new Intl.NumberFormat(void 0, {
|
|
36
|
+
style: "currency",
|
|
37
|
+
currency: currencyCode
|
|
38
|
+
}).format(cents / 100);
|
|
39
|
+
} catch {
|
|
40
|
+
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
45
|
+
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
46
|
+
function bundleLineAttributes(bundleGid, bundleType) {
|
|
47
|
+
return [
|
|
48
|
+
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
49
|
+
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
// ../render/src/adapt.ts
|
|
32
54
|
var import_core = require("@lime-bundles/core");
|
|
33
55
|
function numericId(gid) {
|
|
@@ -230,28 +252,6 @@ function recalcPricing(container, products, formatMoney) {
|
|
|
230
252
|
}
|
|
231
253
|
}
|
|
232
254
|
|
|
233
|
-
// ../render/src/host.ts
|
|
234
|
-
function intlFormatMoney(currencyCode) {
|
|
235
|
-
return (cents) => {
|
|
236
|
-
try {
|
|
237
|
-
return new Intl.NumberFormat(void 0, {
|
|
238
|
-
style: "currency",
|
|
239
|
-
currency: currencyCode
|
|
240
|
-
}).format(cents / 100);
|
|
241
|
-
} catch {
|
|
242
|
-
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
247
|
-
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
248
|
-
function bundleLineAttributes(bundleGid, bundleType) {
|
|
249
|
-
return [
|
|
250
|
-
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
251
|
-
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
252
|
-
];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
255
|
// ../render/src/product/option-selects.ts
|
|
256
256
|
function bindVariantSelects(row, getProduct, onSelected) {
|
|
257
257
|
const optionSelects = row.querySelectorAll("[data-variant-option]");
|
|
@@ -2939,9 +2939,9 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
|
|
|
2939
2939
|
}
|
|
2940
2940
|
|
|
2941
2941
|
// src/renderers/volume.ts
|
|
2942
|
-
function renderVolumeBundle(container, bundle, onAddToCart) {
|
|
2942
|
+
function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
|
|
2943
2943
|
const wc = bundle.widgetConfig;
|
|
2944
|
-
const product = bundle.products
|
|
2944
|
+
const product = (0, import_core4.resolveContextProduct)(bundle.products, productContext);
|
|
2945
2945
|
if (!product) return;
|
|
2946
2946
|
const variants = product.variants.nodes;
|
|
2947
2947
|
const minTierQty = Math.min(
|
|
@@ -6737,6 +6737,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6737
6737
|
"storefront-token",
|
|
6738
6738
|
"bundle-gid",
|
|
6739
6739
|
"product-handle",
|
|
6740
|
+
"product-id",
|
|
6740
6741
|
"app-url",
|
|
6741
6742
|
"analytics",
|
|
6742
6743
|
"locale",
|
|
@@ -6797,7 +6798,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6797
6798
|
}
|
|
6798
6799
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
6799
6800
|
if (oldValue === newValue || !this.isConnected) return;
|
|
6800
|
-
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
6801
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
|
|
6801
6802
|
if (this.shopDomain && this.storefrontToken) {
|
|
6802
6803
|
this.fetchBundle();
|
|
6803
6804
|
}
|
|
@@ -6815,6 +6816,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6815
6816
|
get productHandleAttr() {
|
|
6816
6817
|
return this.getAttribute("product-handle") ?? "";
|
|
6817
6818
|
}
|
|
6819
|
+
/**
|
|
6820
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
6821
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
6822
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
6823
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
6824
|
+
* the bundle's first product.
|
|
6825
|
+
*/
|
|
6826
|
+
get productIdAttr() {
|
|
6827
|
+
return this.getAttribute("product-id") ?? "";
|
|
6828
|
+
}
|
|
6818
6829
|
get appUrl() {
|
|
6819
6830
|
return this.getAttribute("app-url") ?? "";
|
|
6820
6831
|
}
|
|
@@ -6988,7 +6999,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6988
6999
|
});
|
|
6989
7000
|
const storage = window.localStorage;
|
|
6990
7001
|
const key = cartStorageKey(this.shopDomain);
|
|
6991
|
-
|
|
7002
|
+
let cartId = storage?.getItem(key) ?? null;
|
|
6992
7003
|
const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
|
|
6993
7004
|
const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
|
|
6994
7005
|
const fail = (message) => {
|
|
@@ -7002,10 +7013,17 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7002
7013
|
};
|
|
7003
7014
|
try {
|
|
7004
7015
|
let checkoutUrl = null;
|
|
7005
|
-
if (
|
|
7016
|
+
if (cartId) {
|
|
7017
|
+
cartId = await this.clearAbandonedBundleLines(
|
|
7018
|
+
client,
|
|
7019
|
+
cartId,
|
|
7020
|
+
() => storage?.removeItem(key)
|
|
7021
|
+
);
|
|
7022
|
+
}
|
|
7023
|
+
if (cartId) {
|
|
7006
7024
|
const res = await client.query(
|
|
7007
7025
|
import_core6.CART_LINES_ADD_MUTATION,
|
|
7008
|
-
{ cartId
|
|
7026
|
+
{ cartId, lines }
|
|
7009
7027
|
);
|
|
7010
7028
|
const payload = res.cartLinesAdd;
|
|
7011
7029
|
if (payload?.userErrors?.length) {
|
|
@@ -7058,6 +7076,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7058
7076
|
);
|
|
7059
7077
|
}
|
|
7060
7078
|
}
|
|
7079
|
+
/**
|
|
7080
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
7081
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
7082
|
+
* clicked add three times while deciding checks out with only what they
|
|
7083
|
+
* just built. Lines without the tag were put in the cart by something
|
|
7084
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
7085
|
+
* left alone.
|
|
7086
|
+
*
|
|
7087
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
7088
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
7089
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
7090
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
7091
|
+
* checkout matches the shopper's current build.
|
|
7092
|
+
*/
|
|
7093
|
+
async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
|
|
7094
|
+
const res = await client.query(import_core6.CART_LINES_QUERY, {
|
|
7095
|
+
cartId
|
|
7096
|
+
});
|
|
7097
|
+
if (!res.cart) {
|
|
7098
|
+
dropSavedCart();
|
|
7099
|
+
return null;
|
|
7100
|
+
}
|
|
7101
|
+
const abandonedLineIds = res.cart.lines.nodes.filter(
|
|
7102
|
+
(line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
|
|
7103
|
+
).map((line) => line.id);
|
|
7104
|
+
if (abandonedLineIds.length === 0) return cartId;
|
|
7105
|
+
const removal = await client.query(
|
|
7106
|
+
import_core6.CART_LINES_REMOVE_MUTATION,
|
|
7107
|
+
{ cartId, lineIds: abandonedLineIds }
|
|
7108
|
+
);
|
|
7109
|
+
if (removal.cartLinesRemove?.userErrors?.length) {
|
|
7110
|
+
dropSavedCart();
|
|
7111
|
+
return null;
|
|
7112
|
+
}
|
|
7113
|
+
return cartId;
|
|
7114
|
+
}
|
|
7061
7115
|
reportAddToCartEvent(bundle, lines) {
|
|
7062
7116
|
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
7063
7117
|
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
@@ -7129,7 +7183,10 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7129
7183
|
);
|
|
7130
7184
|
break;
|
|
7131
7185
|
case "volume":
|
|
7132
|
-
renderVolumeBundle(container, bundle, dispatch
|
|
7186
|
+
renderVolumeBundle(container, bundle, dispatch, {
|
|
7187
|
+
productId: this.productIdAttr || null,
|
|
7188
|
+
productHandle: this.currentProductHandle
|
|
7189
|
+
});
|
|
7133
7190
|
break;
|
|
7134
7191
|
case "bogo":
|
|
7135
7192
|
renderBogoBundle(container, bundle, dispatch, registerCleanup);
|
package/dist/index.d.cts
CHANGED
|
@@ -26,6 +26,10 @@ import { BuyerResolver } from '@lime-bundles/core';
|
|
|
26
26
|
* 3. /products/<handle> segment of window.location.pathname
|
|
27
27
|
* 4. fallthrough: renders nothing, fires `lime-bundle:error`
|
|
28
28
|
*
|
|
29
|
+
* Multi-product renderers (volume) bind to the product this embed is
|
|
30
|
+
* standing on: the explicit `product-id` attribute wins, then the handle
|
|
31
|
+
* cascade above, then the bundle's first product when neither names one.
|
|
32
|
+
*
|
|
29
33
|
* ## Add-to-cart behaviour
|
|
30
34
|
*
|
|
31
35
|
* Merchants who do nothing get a default: the widget calls Shopify's
|
|
@@ -92,6 +96,14 @@ declare class LimeBundleElement extends HTMLElement {
|
|
|
92
96
|
private get storefrontToken();
|
|
93
97
|
private get bundleGid();
|
|
94
98
|
private get productHandleAttr();
|
|
99
|
+
/**
|
|
100
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
101
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
102
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
103
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
104
|
+
* the bundle's first product.
|
|
105
|
+
*/
|
|
106
|
+
private get productIdAttr();
|
|
95
107
|
private get appUrl();
|
|
96
108
|
private get analyticsEnabled();
|
|
97
109
|
private get country();
|
|
@@ -128,6 +140,21 @@ declare class LimeBundleElement extends HTMLElement {
|
|
|
128
140
|
* instead of creating a new one every click.
|
|
129
141
|
*/
|
|
130
142
|
private defaultAddToCart;
|
|
143
|
+
/**
|
|
144
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
145
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
146
|
+
* clicked add three times while deciding checks out with only what they
|
|
147
|
+
* just built. Lines without the tag were put in the cart by something
|
|
148
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
149
|
+
* left alone.
|
|
150
|
+
*
|
|
151
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
152
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
153
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
154
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
155
|
+
* checkout matches the shopper's current build.
|
|
156
|
+
*/
|
|
157
|
+
private clearAbandonedBundleLines;
|
|
131
158
|
private reportAddToCartEvent;
|
|
132
159
|
private renderBundles;
|
|
133
160
|
private setupImpressionFor;
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ import { BuyerResolver } from '@lime-bundles/core';
|
|
|
26
26
|
* 3. /products/<handle> segment of window.location.pathname
|
|
27
27
|
* 4. fallthrough: renders nothing, fires `lime-bundle:error`
|
|
28
28
|
*
|
|
29
|
+
* Multi-product renderers (volume) bind to the product this embed is
|
|
30
|
+
* standing on: the explicit `product-id` attribute wins, then the handle
|
|
31
|
+
* cascade above, then the bundle's first product when neither names one.
|
|
32
|
+
*
|
|
29
33
|
* ## Add-to-cart behaviour
|
|
30
34
|
*
|
|
31
35
|
* Merchants who do nothing get a default: the widget calls Shopify's
|
|
@@ -92,6 +96,14 @@ declare class LimeBundleElement extends HTMLElement {
|
|
|
92
96
|
private get storefrontToken();
|
|
93
97
|
private get bundleGid();
|
|
94
98
|
private get productHandleAttr();
|
|
99
|
+
/**
|
|
100
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
101
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
102
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
103
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
104
|
+
* the bundle's first product.
|
|
105
|
+
*/
|
|
106
|
+
private get productIdAttr();
|
|
95
107
|
private get appUrl();
|
|
96
108
|
private get analyticsEnabled();
|
|
97
109
|
private get country();
|
|
@@ -128,6 +140,21 @@ declare class LimeBundleElement extends HTMLElement {
|
|
|
128
140
|
* instead of creating a new one every click.
|
|
129
141
|
*/
|
|
130
142
|
private defaultAddToCart;
|
|
143
|
+
/**
|
|
144
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
145
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
146
|
+
* clicked add three times while deciding checks out with only what they
|
|
147
|
+
* just built. Lines without the tag were put in the cart by something
|
|
148
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
149
|
+
* left alone.
|
|
150
|
+
*
|
|
151
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
152
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
153
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
154
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
155
|
+
* checkout matches the shopper's current build.
|
|
156
|
+
*/
|
|
157
|
+
private clearAbandonedBundleLines;
|
|
131
158
|
private reportAddToCartEvent;
|
|
132
159
|
private renderBundles;
|
|
133
160
|
private setupImpressionFor;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
BUNDLES_FOR_PRODUCT_QUERY,
|
|
6
6
|
CART_CREATE_MUTATION,
|
|
7
7
|
CART_LINES_ADD_MUTATION,
|
|
8
|
+
CART_LINES_QUERY,
|
|
9
|
+
CART_LINES_REMOVE_MUTATION,
|
|
8
10
|
SHOP_SETTINGS_QUERY,
|
|
9
11
|
parseMetaobjectBundle,
|
|
10
12
|
observeImpression,
|
|
@@ -19,6 +21,28 @@ import {
|
|
|
19
21
|
resolveAbTests
|
|
20
22
|
} from "@lime-bundles/core";
|
|
21
23
|
|
|
24
|
+
// ../render/src/host.ts
|
|
25
|
+
function intlFormatMoney(currencyCode) {
|
|
26
|
+
return (cents) => {
|
|
27
|
+
try {
|
|
28
|
+
return new Intl.NumberFormat(void 0, {
|
|
29
|
+
style: "currency",
|
|
30
|
+
currency: currencyCode
|
|
31
|
+
}).format(cents / 100);
|
|
32
|
+
} catch {
|
|
33
|
+
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
38
|
+
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
39
|
+
function bundleLineAttributes(bundleGid, bundleType) {
|
|
40
|
+
return [
|
|
41
|
+
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
42
|
+
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
// ../render/src/adapt.ts
|
|
23
47
|
import {
|
|
24
48
|
formatUnitPrice,
|
|
@@ -226,28 +250,6 @@ function recalcPricing(container, products, formatMoney) {
|
|
|
226
250
|
}
|
|
227
251
|
}
|
|
228
252
|
|
|
229
|
-
// ../render/src/host.ts
|
|
230
|
-
function intlFormatMoney(currencyCode) {
|
|
231
|
-
return (cents) => {
|
|
232
|
-
try {
|
|
233
|
-
return new Intl.NumberFormat(void 0, {
|
|
234
|
-
style: "currency",
|
|
235
|
-
currency: currencyCode
|
|
236
|
-
}).format(cents / 100);
|
|
237
|
-
} catch {
|
|
238
|
-
return currencyCode + " " + (cents / 100).toFixed(2);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
var BUNDLE_GID_ATTRIBUTE = "_lime_bundle_gid";
|
|
243
|
-
var BUNDLE_TYPE_ATTRIBUTE = "_lime_bundle_type";
|
|
244
|
-
function bundleLineAttributes(bundleGid, bundleType) {
|
|
245
|
-
return [
|
|
246
|
-
{ key: BUNDLE_GID_ATTRIBUTE, value: bundleGid },
|
|
247
|
-
{ key: BUNDLE_TYPE_ATTRIBUTE, value: bundleType }
|
|
248
|
-
];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
253
|
// ../render/src/product/option-selects.ts
|
|
252
254
|
function bindVariantSelects(row, getProduct, onSelected) {
|
|
253
255
|
const optionSelects = row.querySelectorAll("[data-variant-option]");
|
|
@@ -2809,6 +2811,7 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup, current
|
|
|
2809
2811
|
import {
|
|
2810
2812
|
bestValueTierIndex,
|
|
2811
2813
|
isVariantFulfillable as isVariantFulfillable2,
|
|
2814
|
+
resolveContextProduct,
|
|
2812
2815
|
resolveDefaultTierIndex,
|
|
2813
2816
|
resolvePopularTierIndex,
|
|
2814
2817
|
stockCapForVariant as stockCapForVariant2
|
|
@@ -2941,9 +2944,9 @@ function selectTier(container, allTiers, basePrice, discountType, index, transla
|
|
|
2941
2944
|
}
|
|
2942
2945
|
|
|
2943
2946
|
// src/renderers/volume.ts
|
|
2944
|
-
function renderVolumeBundle(container, bundle, onAddToCart) {
|
|
2947
|
+
function renderVolumeBundle(container, bundle, onAddToCart, productContext) {
|
|
2945
2948
|
const wc = bundle.widgetConfig;
|
|
2946
|
-
const product = bundle.products
|
|
2949
|
+
const product = resolveContextProduct(bundle.products, productContext);
|
|
2947
2950
|
if (!product) return;
|
|
2948
2951
|
const variants = product.variants.nodes;
|
|
2949
2952
|
const minTierQty = Math.min(
|
|
@@ -6742,6 +6745,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6742
6745
|
"storefront-token",
|
|
6743
6746
|
"bundle-gid",
|
|
6744
6747
|
"product-handle",
|
|
6748
|
+
"product-id",
|
|
6745
6749
|
"app-url",
|
|
6746
6750
|
"analytics",
|
|
6747
6751
|
"locale",
|
|
@@ -6802,7 +6806,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6802
6806
|
}
|
|
6803
6807
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
6804
6808
|
if (oldValue === newValue || !this.isConnected) return;
|
|
6805
|
-
if (name === "bundle-gid" || name === "product-handle" || name === "shop-domain" || name === "storefront-token") {
|
|
6809
|
+
if (name === "bundle-gid" || name === "product-handle" || name === "product-id" || name === "shop-domain" || name === "storefront-token") {
|
|
6806
6810
|
if (this.shopDomain && this.storefrontToken) {
|
|
6807
6811
|
this.fetchBundle();
|
|
6808
6812
|
}
|
|
@@ -6820,6 +6824,16 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6820
6824
|
get productHandleAttr() {
|
|
6821
6825
|
return this.getAttribute("product-handle") ?? "";
|
|
6822
6826
|
}
|
|
6827
|
+
/**
|
|
6828
|
+
* The current product's id — full GID (`gid://shopify/Product/123`) or
|
|
6829
|
+
* bare numeric id. Tells multi-product renderers (volume) which of the
|
|
6830
|
+
* bundle's products this embed is standing on; takes precedence over the
|
|
6831
|
+
* handle cascade. Without it (and with no resolvable handle) they bind to
|
|
6832
|
+
* the bundle's first product.
|
|
6833
|
+
*/
|
|
6834
|
+
get productIdAttr() {
|
|
6835
|
+
return this.getAttribute("product-id") ?? "";
|
|
6836
|
+
}
|
|
6823
6837
|
get appUrl() {
|
|
6824
6838
|
return this.getAttribute("app-url") ?? "";
|
|
6825
6839
|
}
|
|
@@ -6993,7 +7007,7 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
6993
7007
|
});
|
|
6994
7008
|
const storage = window.localStorage;
|
|
6995
7009
|
const key = cartStorageKey(this.shopDomain);
|
|
6996
|
-
|
|
7010
|
+
let cartId = storage?.getItem(key) ?? null;
|
|
6997
7011
|
const isStaleCartError = (errors) => errors.length > 0 && errors.every((e) => (e.field ?? []).includes("cartId"));
|
|
6998
7012
|
const stockCapped = (warnings) => (warnings ?? []).some((w) => w.code === "MERCHANDISE_NOT_ENOUGH_STOCK");
|
|
6999
7013
|
const fail = (message) => {
|
|
@@ -7007,10 +7021,17 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7007
7021
|
};
|
|
7008
7022
|
try {
|
|
7009
7023
|
let checkoutUrl = null;
|
|
7010
|
-
if (
|
|
7024
|
+
if (cartId) {
|
|
7025
|
+
cartId = await this.clearAbandonedBundleLines(
|
|
7026
|
+
client,
|
|
7027
|
+
cartId,
|
|
7028
|
+
() => storage?.removeItem(key)
|
|
7029
|
+
);
|
|
7030
|
+
}
|
|
7031
|
+
if (cartId) {
|
|
7011
7032
|
const res = await client.query(
|
|
7012
7033
|
CART_LINES_ADD_MUTATION,
|
|
7013
|
-
{ cartId
|
|
7034
|
+
{ cartId, lines }
|
|
7014
7035
|
);
|
|
7015
7036
|
const payload = res.cartLinesAdd;
|
|
7016
7037
|
if (payload?.userErrors?.length) {
|
|
@@ -7063,6 +7084,42 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7063
7084
|
);
|
|
7064
7085
|
}
|
|
7065
7086
|
}
|
|
7087
|
+
/**
|
|
7088
|
+
* Remove this widget's own earlier lines — the ones tagged with the
|
|
7089
|
+
* `_lime_bundle_gid` attribute — from the saved cart, so a shopper who
|
|
7090
|
+
* clicked add three times while deciding checks out with only what they
|
|
7091
|
+
* just built. Lines without the tag were put in the cart by something
|
|
7092
|
+
* else (the host site, a previous non-widget purchase flow) and are
|
|
7093
|
+
* left alone.
|
|
7094
|
+
*
|
|
7095
|
+
* Returns the cart id when the cart is still usable. Returns null after
|
|
7096
|
+
* calling `dropSavedCart` when it isn't: either the id no longer
|
|
7097
|
+
* resolves (expired or merged on Shopify's side), or the removal itself
|
|
7098
|
+
* failed — in both cases a fresh cart is the only way to guarantee the
|
|
7099
|
+
* checkout matches the shopper's current build.
|
|
7100
|
+
*/
|
|
7101
|
+
async clearAbandonedBundleLines(client, cartId, dropSavedCart) {
|
|
7102
|
+
const res = await client.query(CART_LINES_QUERY, {
|
|
7103
|
+
cartId
|
|
7104
|
+
});
|
|
7105
|
+
if (!res.cart) {
|
|
7106
|
+
dropSavedCart();
|
|
7107
|
+
return null;
|
|
7108
|
+
}
|
|
7109
|
+
const abandonedLineIds = res.cart.lines.nodes.filter(
|
|
7110
|
+
(line) => line.attributes.some((attr) => attr.key === BUNDLE_GID_ATTRIBUTE)
|
|
7111
|
+
).map((line) => line.id);
|
|
7112
|
+
if (abandonedLineIds.length === 0) return cartId;
|
|
7113
|
+
const removal = await client.query(
|
|
7114
|
+
CART_LINES_REMOVE_MUTATION,
|
|
7115
|
+
{ cartId, lineIds: abandonedLineIds }
|
|
7116
|
+
);
|
|
7117
|
+
if (removal.cartLinesRemove?.userErrors?.length) {
|
|
7118
|
+
dropSavedCart();
|
|
7119
|
+
return null;
|
|
7120
|
+
}
|
|
7121
|
+
return cartId;
|
|
7122
|
+
}
|
|
7066
7123
|
reportAddToCartEvent(bundle, lines) {
|
|
7067
7124
|
if (!this.analyticsEnabled || !this.appUrl) return;
|
|
7068
7125
|
const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
@@ -7134,7 +7191,10 @@ var LimeBundleElement = class extends HTMLElement {
|
|
|
7134
7191
|
);
|
|
7135
7192
|
break;
|
|
7136
7193
|
case "volume":
|
|
7137
|
-
renderVolumeBundle(container, bundle, dispatch
|
|
7194
|
+
renderVolumeBundle(container, bundle, dispatch, {
|
|
7195
|
+
productId: this.productIdAttr || null,
|
|
7196
|
+
productHandle: this.currentProductHandle
|
|
7197
|
+
});
|
|
7138
7198
|
break;
|
|
7139
7199
|
case "bogo":
|
|
7140
7200
|
renderBogoBundle(container, bundle, dispatch, registerCleanup);
|