@lime-bundles/react 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 +132 -0
- package/dist/index.cjs +329 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -14
- package/dist/index.d.ts +74 -14
- package/dist/index.js +315 -217
- package/dist/index.js.map +1 -1
- package/package.json +3 -7
- package/dist/hooks/useCart.cjs +0 -84
- package/dist/hooks/useCart.cjs.map +0 -1
- package/dist/hooks/useCart.d.cts +0 -19
- package/dist/hooks/useCart.d.ts +0 -19
- package/dist/hooks/useCart.js +0 -64
- package/dist/hooks/useCart.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -18,151 +18,153 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
20
|
// src/index.ts
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BundleParseError: () => import_core8.BundleParseError,
|
|
23
24
|
FixedBundle: () => FixedBundle,
|
|
24
25
|
MixMatchBundle: () => MixMatchBundle,
|
|
26
|
+
StorefrontApiError: () => import_core8.StorefrontApiError,
|
|
25
27
|
VolumeBundle: () => VolumeBundle,
|
|
28
|
+
fetchBundleData: () => fetchBundleData,
|
|
29
|
+
fetchShopCustomCss: () => fetchShopCustomCss,
|
|
26
30
|
useAnalytics: () => useAnalytics,
|
|
27
|
-
useBundleData: () => useBundleData
|
|
28
|
-
useCart: () => useCart
|
|
31
|
+
useBundleData: () => useBundleData
|
|
29
32
|
});
|
|
30
|
-
module.exports = __toCommonJS(
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
34
|
|
|
32
35
|
// src/components/FixedBundle.tsx
|
|
33
|
-
var
|
|
34
|
-
var
|
|
36
|
+
var import_react3 = require("react");
|
|
37
|
+
var import_core5 = require("@lime-bundles/core");
|
|
35
38
|
|
|
36
39
|
// src/hooks/useBundleData.ts
|
|
37
40
|
var import_react = require("react");
|
|
41
|
+
var import_core3 = require("@lime-bundles/core");
|
|
42
|
+
|
|
43
|
+
// src/fetchBundleData.ts
|
|
38
44
|
var import_core = require("@lime-bundles/core");
|
|
45
|
+
async function fetchBundleData(options) {
|
|
46
|
+
const client = (0, import_core.createStorefrontClient)({
|
|
47
|
+
shopDomain: options.shopDomain,
|
|
48
|
+
accessToken: options.storefrontAccessToken,
|
|
49
|
+
buyerIp: options.buyerIp,
|
|
50
|
+
apiVersion: options.apiVersion
|
|
51
|
+
});
|
|
52
|
+
const data = await client.query(
|
|
53
|
+
import_core.BUNDLE_METAOBJECT_QUERY,
|
|
54
|
+
{ id: options.bundleGid },
|
|
55
|
+
{ signal: options.signal }
|
|
56
|
+
);
|
|
57
|
+
if (!data.metaobject) {
|
|
58
|
+
throw new import_core.BundleParseError(
|
|
59
|
+
`Bundle not found: ${options.bundleGid}`,
|
|
60
|
+
"not_found"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return (0, import_core.parseMetaobjectBundleStrict)(
|
|
64
|
+
data.metaobject.id,
|
|
65
|
+
data.metaobject.fields
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/fetchShopCustomCss.ts
|
|
70
|
+
var import_core2 = require("@lime-bundles/core");
|
|
71
|
+
var cache = /* @__PURE__ */ new Map();
|
|
72
|
+
async function fetchShopCustomCss(options) {
|
|
73
|
+
const cached = cache.get(options.shopDomain);
|
|
74
|
+
if (cached) return cached;
|
|
75
|
+
const promise = (async () => {
|
|
76
|
+
const client = (0, import_core2.createStorefrontClient)({
|
|
77
|
+
shopDomain: options.shopDomain,
|
|
78
|
+
accessToken: options.storefrontAccessToken,
|
|
79
|
+
buyerIp: options.buyerIp
|
|
80
|
+
});
|
|
81
|
+
const data = await client.query(
|
|
82
|
+
import_core2.SHOP_CUSTOM_CSS_QUERY,
|
|
83
|
+
void 0,
|
|
84
|
+
{ signal: options.signal }
|
|
85
|
+
);
|
|
86
|
+
return data.shop?.metafield?.value ?? null;
|
|
87
|
+
})();
|
|
88
|
+
cache.set(options.shopDomain, promise);
|
|
89
|
+
promise.catch(() => {
|
|
90
|
+
if (cache.get(options.shopDomain) === promise) {
|
|
91
|
+
cache.delete(options.shopDomain);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return promise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/hooks/useBundleData.ts
|
|
98
|
+
var INITIAL_STATE = {
|
|
99
|
+
status: "loading",
|
|
100
|
+
bundle: null,
|
|
101
|
+
error: null
|
|
102
|
+
};
|
|
39
103
|
function useBundleData(options) {
|
|
40
|
-
const [
|
|
41
|
-
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
42
|
-
const [error, setError] = (0, import_react.useState)(null);
|
|
43
|
-
const abortRef = (0, import_react.useRef)(null);
|
|
104
|
+
const [state, setState] = (0, import_react.useState)(INITIAL_STATE);
|
|
44
105
|
(0, import_react.useEffect)(() => {
|
|
45
|
-
abortRef.current?.abort();
|
|
46
106
|
const controller = new AbortController();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
setError(null);
|
|
50
|
-
const client = (0, import_core.createStorefrontClient)({
|
|
107
|
+
setState(INITIAL_STATE);
|
|
108
|
+
fetchBundleData({
|
|
51
109
|
shopDomain: options.shopDomain,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}).then((data) => {
|
|
110
|
+
storefrontAccessToken: options.storefrontAccessToken,
|
|
111
|
+
bundleGid: options.bundleGid,
|
|
112
|
+
signal: controller.signal
|
|
113
|
+
}).then((bundle) => {
|
|
57
114
|
if (controller.signal.aborted) return;
|
|
58
|
-
|
|
59
|
-
setError("Bundle not found");
|
|
60
|
-
setBundle(null);
|
|
61
|
-
} else {
|
|
62
|
-
const parsed = (0, import_core.parseMetaobjectBundle)(
|
|
63
|
-
data.metaobject.id,
|
|
64
|
-
data.metaobject.fields
|
|
65
|
-
);
|
|
66
|
-
if (parsed) {
|
|
67
|
-
setBundle(parsed);
|
|
68
|
-
} else {
|
|
69
|
-
setError("Bundle is not active or has expired");
|
|
70
|
-
setBundle(null);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
setLoading(false);
|
|
115
|
+
setState({ status: "success", bundle, error: null });
|
|
74
116
|
}).catch((err) => {
|
|
75
117
|
if (controller.signal.aborted) return;
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
setState({
|
|
119
|
+
status: "error",
|
|
120
|
+
bundle: null,
|
|
121
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
fetchShopCustomCss({
|
|
125
|
+
shopDomain: options.shopDomain,
|
|
126
|
+
storefrontAccessToken: options.storefrontAccessToken,
|
|
127
|
+
signal: controller.signal
|
|
128
|
+
}).then((css) => {
|
|
129
|
+
if (controller.signal.aborted) return;
|
|
130
|
+
(0, import_core3.injectCustomCss)(options.shopDomain, css);
|
|
131
|
+
}).catch(() => {
|
|
78
132
|
});
|
|
79
133
|
return () => {
|
|
80
134
|
controller.abort();
|
|
81
135
|
};
|
|
82
|
-
}, [options.shopDomain, options.storefrontAccessToken, options.bundleGid]);
|
|
83
|
-
return { bundle, loading, error };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// src/hooks/useCart.ts
|
|
87
|
-
var import_react2 = require("react");
|
|
88
|
-
var import_core2 = require("@lime-bundles/core");
|
|
89
|
-
function useCart(options) {
|
|
90
|
-
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
91
|
-
const [error, setError] = (0, import_react2.useState)(null);
|
|
92
|
-
const [lastResult, setLastResult] = (0, import_react2.useState)(null);
|
|
93
|
-
const cart = (0, import_react2.useMemo)(() => {
|
|
94
|
-
const apiType = options.cartApi ?? (0, import_core2.detectCartApi)();
|
|
95
|
-
if (apiType === "ajax") {
|
|
96
|
-
return (0, import_core2.createAjaxCartApi)(options.bundleGid, options.bundleType);
|
|
97
|
-
}
|
|
98
|
-
const client = (0, import_core2.createStorefrontClient)({
|
|
99
|
-
shopDomain: options.shopDomain,
|
|
100
|
-
accessToken: options.storefrontAccessToken
|
|
101
|
-
});
|
|
102
|
-
return (0, import_core2.createStorefrontCartApi)(
|
|
103
|
-
client,
|
|
104
|
-
options.bundleGid,
|
|
105
|
-
options.bundleType,
|
|
106
|
-
options.cartId
|
|
107
|
-
);
|
|
108
136
|
}, [
|
|
109
137
|
options.shopDomain,
|
|
110
138
|
options.storefrontAccessToken,
|
|
111
|
-
options.bundleGid
|
|
112
|
-
options.bundleType,
|
|
113
|
-
options.cartId,
|
|
114
|
-
options.cartApi
|
|
139
|
+
options.bundleGid
|
|
115
140
|
]);
|
|
116
|
-
|
|
117
|
-
async (items) => {
|
|
118
|
-
setLoading(true);
|
|
119
|
-
setError(null);
|
|
120
|
-
try {
|
|
121
|
-
const result = await cart.addLines(items);
|
|
122
|
-
setLastResult(result);
|
|
123
|
-
if (!result.success) {
|
|
124
|
-
setError(result.error ?? "Failed to add to cart");
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
} catch (err) {
|
|
128
|
-
const message = err instanceof Error ? err.message : "Failed to add to cart";
|
|
129
|
-
setError(message);
|
|
130
|
-
const result = { success: false, error: message };
|
|
131
|
-
setLastResult(result);
|
|
132
|
-
return result;
|
|
133
|
-
} finally {
|
|
134
|
-
setLoading(false);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
[cart]
|
|
138
|
-
);
|
|
139
|
-
return { addToCart, loading, error, lastResult };
|
|
141
|
+
return state;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
// src/hooks/useAnalytics.ts
|
|
143
|
-
var
|
|
144
|
-
var
|
|
145
|
+
var import_react2 = require("react");
|
|
146
|
+
var import_core4 = require("@lime-bundles/core");
|
|
145
147
|
function useAnalytics(options) {
|
|
146
|
-
const impressionFiredRef = (0,
|
|
147
|
-
const [element, setElement] = (0,
|
|
148
|
-
const elementRef = (0,
|
|
148
|
+
const impressionFiredRef = (0, import_react2.useRef)(false);
|
|
149
|
+
const [element, setElement] = (0, import_react2.useState)(null);
|
|
150
|
+
const elementRef = (0, import_react2.useCallback)((el) => {
|
|
149
151
|
setElement(el);
|
|
150
152
|
}, []);
|
|
151
|
-
const config = (0,
|
|
153
|
+
const config = (0, import_react2.useMemo)(
|
|
152
154
|
() => ({ shopDomain: options.shopDomain, appUrl: options.appUrl }),
|
|
153
155
|
[options.shopDomain, options.appUrl]
|
|
154
156
|
);
|
|
155
|
-
(0,
|
|
157
|
+
(0, import_react2.useEffect)(() => {
|
|
156
158
|
impressionFiredRef.current = false;
|
|
157
159
|
}, [options.bundleGid, options.abVariant]);
|
|
158
|
-
(0,
|
|
160
|
+
(0, import_react2.useEffect)(() => {
|
|
159
161
|
if (options.enabled === false) return;
|
|
160
162
|
if (impressionFiredRef.current) return;
|
|
161
163
|
if (!element) return;
|
|
162
|
-
const cleanup = (0,
|
|
164
|
+
const cleanup = (0, import_core4.observeImpression)(element, () => {
|
|
163
165
|
if (impressionFiredRef.current) return;
|
|
164
166
|
impressionFiredRef.current = true;
|
|
165
|
-
(0,
|
|
167
|
+
(0, import_core4.reportImpression)(config, {
|
|
166
168
|
bundleGid: options.bundleGid,
|
|
167
169
|
bundleType: options.bundleType,
|
|
168
170
|
abTestId: options.abTestId,
|
|
@@ -179,10 +181,10 @@ function useAnalytics(options) {
|
|
|
179
181
|
options.abTestId,
|
|
180
182
|
options.abVariant
|
|
181
183
|
]);
|
|
182
|
-
const trackAddToCart = (0,
|
|
184
|
+
const trackAddToCart = (0, import_react2.useCallback)(
|
|
183
185
|
(event) => {
|
|
184
186
|
if (options.enabled === false) return;
|
|
185
|
-
(0,
|
|
187
|
+
(0, import_core4.reportAddToCart)(config, {
|
|
186
188
|
bundleGid: options.bundleGid,
|
|
187
189
|
bundleType: options.bundleType,
|
|
188
190
|
productId: event.productId,
|
|
@@ -211,27 +213,17 @@ function FixedBundle(props) {
|
|
|
211
213
|
shopDomain,
|
|
212
214
|
storefrontAccessToken,
|
|
213
215
|
bundleGid,
|
|
214
|
-
cartId,
|
|
215
|
-
cartApi,
|
|
216
216
|
appUrl,
|
|
217
217
|
analyticsEnabled,
|
|
218
218
|
onAddToCart,
|
|
219
219
|
onError,
|
|
220
220
|
className
|
|
221
221
|
} = props;
|
|
222
|
-
const
|
|
222
|
+
const result = useBundleData({
|
|
223
223
|
shopDomain,
|
|
224
224
|
storefrontAccessToken,
|
|
225
225
|
bundleGid
|
|
226
226
|
});
|
|
227
|
-
const { addToCart, loading: cartLoading, error: cartError } = useCart({
|
|
228
|
-
shopDomain,
|
|
229
|
-
storefrontAccessToken,
|
|
230
|
-
bundleGid,
|
|
231
|
-
bundleType: "fixed",
|
|
232
|
-
cartId,
|
|
233
|
-
cartApi
|
|
234
|
-
});
|
|
235
227
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
236
228
|
shopDomain,
|
|
237
229
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
@@ -239,37 +231,69 @@ function FixedBundle(props) {
|
|
|
239
231
|
bundleType: "fixed",
|
|
240
232
|
enabled: analyticsEnabled !== false
|
|
241
233
|
});
|
|
242
|
-
const
|
|
234
|
+
const [addingToCart, setAddingToCart] = (0, import_react3.useState)(false);
|
|
235
|
+
const [cartError, setCartError] = (0, import_react3.useState)(null);
|
|
236
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
|
|
237
|
+
(0, import_react3.useEffect)(() => {
|
|
238
|
+
if (result.status === "error") {
|
|
239
|
+
onError?.(result.error);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (result.status === "success" && result.bundle.bundleType !== "fixed") {
|
|
243
|
+
onError?.(
|
|
244
|
+
new Error(
|
|
245
|
+
`FixedBundle: expected bundleType="fixed", got "${result.bundle.bundleType}"`
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}, [result, onError]);
|
|
250
|
+
const handleAddToCart = (0, import_react3.useCallback)(async () => {
|
|
243
251
|
if (!bundle) return;
|
|
244
|
-
const
|
|
252
|
+
const lines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
|
|
245
253
|
const variant = p.variants.nodes.find((v) => v.availableForSale);
|
|
246
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
merchandiseId: variant.id,
|
|
256
|
+
quantity: 1,
|
|
257
|
+
attributes: [
|
|
258
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
259
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
260
|
+
]
|
|
261
|
+
};
|
|
247
262
|
});
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
if (lines.length === 0) return;
|
|
264
|
+
setAddingToCart(true);
|
|
265
|
+
setCartError(null);
|
|
266
|
+
try {
|
|
267
|
+
await onAddToCart(lines);
|
|
251
268
|
const totalPrice = bundle.products.reduce((sum, p) => {
|
|
252
269
|
const price = parseFloat(p.priceRange.minVariantPrice.amount);
|
|
253
|
-
return sum + (0,
|
|
270
|
+
return sum + (0, import_core5.calculateDiscount)(
|
|
271
|
+
price,
|
|
272
|
+
bundle.discountConfig.discountType,
|
|
273
|
+
bundle.discountConfig.discountValue
|
|
274
|
+
);
|
|
254
275
|
}, 0);
|
|
255
276
|
trackAddToCart({
|
|
256
277
|
productId: bundle.products[0]?.id ?? "",
|
|
257
278
|
quantity: 1,
|
|
258
279
|
totalPrice: Math.round(totalPrice * 100) / 100
|
|
259
280
|
});
|
|
260
|
-
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
283
|
+
setCartError(error.message);
|
|
284
|
+
onError?.(error);
|
|
285
|
+
} finally {
|
|
286
|
+
setAddingToCart(false);
|
|
261
287
|
}
|
|
262
|
-
}, [bundle,
|
|
263
|
-
if (loading) {
|
|
288
|
+
}, [bundle, onAddToCart, onError, trackAddToCart]);
|
|
289
|
+
if (result.status === "loading") {
|
|
264
290
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
265
291
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
266
292
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
267
293
|
] });
|
|
268
294
|
}
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
295
|
+
if (result.status === "error") return null;
|
|
296
|
+
if (!bundle) return null;
|
|
273
297
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
274
298
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
275
299
|
"div",
|
|
@@ -293,7 +317,7 @@ function FixedBundle(props) {
|
|
|
293
317
|
),
|
|
294
318
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lb-bundle__product-info", children: [
|
|
295
319
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
296
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-price", children: (0,
|
|
320
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core5.formatMoney)(product.priceRange.minVariantPrice.amount, currency) })
|
|
297
321
|
] })
|
|
298
322
|
] }, product.id)) }),
|
|
299
323
|
cartError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
|
|
@@ -302,9 +326,9 @@ function FixedBundle(props) {
|
|
|
302
326
|
{
|
|
303
327
|
className: "lb-bundle__cta",
|
|
304
328
|
onClick: handleAddToCart,
|
|
305
|
-
disabled:
|
|
306
|
-
"aria-busy":
|
|
307
|
-
children:
|
|
329
|
+
disabled: addingToCart,
|
|
330
|
+
"aria-busy": addingToCart,
|
|
331
|
+
children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? "Add Bundle to Cart"
|
|
308
332
|
}
|
|
309
333
|
)
|
|
310
334
|
]
|
|
@@ -313,35 +337,25 @@ function FixedBundle(props) {
|
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
// src/components/MixMatchBundle.tsx
|
|
316
|
-
var
|
|
317
|
-
var
|
|
340
|
+
var import_react4 = require("react");
|
|
341
|
+
var import_core6 = require("@lime-bundles/core");
|
|
318
342
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
319
343
|
function MixMatchBundle(props) {
|
|
320
344
|
const {
|
|
321
345
|
shopDomain,
|
|
322
346
|
storefrontAccessToken,
|
|
323
347
|
bundleGid,
|
|
324
|
-
cartId,
|
|
325
|
-
cartApi,
|
|
326
348
|
appUrl,
|
|
327
349
|
analyticsEnabled,
|
|
328
350
|
onAddToCart,
|
|
329
351
|
onError,
|
|
330
352
|
className
|
|
331
353
|
} = props;
|
|
332
|
-
const
|
|
354
|
+
const result = useBundleData({
|
|
333
355
|
shopDomain,
|
|
334
356
|
storefrontAccessToken,
|
|
335
357
|
bundleGid
|
|
336
358
|
});
|
|
337
|
-
const { addToCart, loading: cartLoading, error: cartError } = useCart({
|
|
338
|
-
shopDomain,
|
|
339
|
-
storefrontAccessToken,
|
|
340
|
-
bundleGid,
|
|
341
|
-
bundleType: "mix_match",
|
|
342
|
-
cartId,
|
|
343
|
-
cartApi
|
|
344
|
-
});
|
|
345
359
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
346
360
|
shopDomain,
|
|
347
361
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
@@ -349,13 +363,31 @@ function MixMatchBundle(props) {
|
|
|
349
363
|
bundleType: "mix_match",
|
|
350
364
|
enabled: analyticsEnabled !== false
|
|
351
365
|
});
|
|
352
|
-
const [selections, setSelections] = (0,
|
|
366
|
+
const [selections, setSelections] = (0, import_react4.useState)(
|
|
367
|
+
/* @__PURE__ */ new Map()
|
|
368
|
+
);
|
|
369
|
+
const [addingToCart, setAddingToCart] = (0, import_react4.useState)(false);
|
|
370
|
+
const [cartError, setCartError] = (0, import_react4.useState)(null);
|
|
371
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
372
|
+
(0, import_react4.useEffect)(() => {
|
|
373
|
+
if (result.status === "error") {
|
|
374
|
+
onError?.(result.error);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
|
|
378
|
+
onError?.(
|
|
379
|
+
new Error(
|
|
380
|
+
`MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}, [result, onError]);
|
|
353
385
|
const totalQuantity = Array.from(selections.values()).reduce(
|
|
354
386
|
(sum, s) => sum + s.quantity,
|
|
355
387
|
0
|
|
356
388
|
);
|
|
357
|
-
const validation = bundle ? (0,
|
|
358
|
-
const toggleProduct = (0,
|
|
389
|
+
const validation = bundle ? (0, import_core6.validateQuantity)(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
|
|
390
|
+
const toggleProduct = (0, import_react4.useCallback)(
|
|
359
391
|
(productId, variant) => {
|
|
360
392
|
setSelections((prev) => {
|
|
361
393
|
const next = new Map(prev);
|
|
@@ -370,7 +402,7 @@ function MixMatchBundle(props) {
|
|
|
370
402
|
},
|
|
371
403
|
[]
|
|
372
404
|
);
|
|
373
|
-
const updateQuantity = (0,
|
|
405
|
+
const updateQuantity = (0, import_react4.useCallback)(
|
|
374
406
|
(productId, variantId, quantity) => {
|
|
375
407
|
setSelections((prev) => {
|
|
376
408
|
const next = new Map(prev);
|
|
@@ -385,42 +417,59 @@ function MixMatchBundle(props) {
|
|
|
385
417
|
},
|
|
386
418
|
[]
|
|
387
419
|
);
|
|
388
|
-
const handleAddToCart = (0,
|
|
420
|
+
const handleAddToCart = (0, import_react4.useCallback)(async () => {
|
|
389
421
|
if (!bundle || !validation.valid) return;
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
quantity: s.quantity
|
|
422
|
+
const lines = Array.from(selections.values()).map((s) => ({
|
|
423
|
+
merchandiseId: s.variantId,
|
|
424
|
+
quantity: s.quantity,
|
|
425
|
+
attributes: [
|
|
426
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
427
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
428
|
+
]
|
|
393
429
|
}));
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
430
|
+
setAddingToCart(true);
|
|
431
|
+
setCartError(null);
|
|
432
|
+
try {
|
|
433
|
+
await onAddToCart(lines);
|
|
434
|
+
const totalPrice = lines.reduce((sum, line) => {
|
|
397
435
|
const product = bundle.products.find(
|
|
398
|
-
(p) => p.variants.nodes.some((v) => v.id ===
|
|
436
|
+
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
399
437
|
);
|
|
400
438
|
const variant = product?.variants.nodes.find(
|
|
401
|
-
(v) => v.id ===
|
|
439
|
+
(v) => v.id === line.merchandiseId
|
|
402
440
|
);
|
|
403
|
-
return sum + parseFloat(variant?.price.amount ?? "0") *
|
|
441
|
+
return sum + parseFloat(variant?.price.amount ?? "0") * line.quantity;
|
|
404
442
|
}, 0);
|
|
405
443
|
trackAddToCart({
|
|
406
444
|
productId: bundle.products[0]?.id ?? "",
|
|
407
445
|
quantity: totalQuantity,
|
|
408
446
|
totalPrice: Math.round(totalPrice * 100) / 100
|
|
409
447
|
});
|
|
410
|
-
onAddToCart?.(items);
|
|
411
448
|
setSelections(/* @__PURE__ */ new Map());
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
451
|
+
setCartError(error.message);
|
|
452
|
+
onError?.(error);
|
|
453
|
+
} finally {
|
|
454
|
+
setAddingToCart(false);
|
|
412
455
|
}
|
|
413
|
-
}, [
|
|
414
|
-
|
|
456
|
+
}, [
|
|
457
|
+
bundle,
|
|
458
|
+
selections,
|
|
459
|
+
validation.valid,
|
|
460
|
+
onAddToCart,
|
|
461
|
+
onError,
|
|
462
|
+
trackAddToCart,
|
|
463
|
+
totalQuantity
|
|
464
|
+
]);
|
|
465
|
+
if (result.status === "loading") {
|
|
415
466
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
416
467
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
417
468
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
418
469
|
] });
|
|
419
470
|
}
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
471
|
+
if (result.status === "error") return null;
|
|
472
|
+
if (!bundle) return null;
|
|
424
473
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
425
474
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
426
475
|
"div",
|
|
@@ -454,14 +503,18 @@ function MixMatchBundle(props) {
|
|
|
454
503
|
),
|
|
455
504
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__product-info", children: [
|
|
456
505
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
457
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: (0,
|
|
506
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "lb-bundle__product-price", children: (0, import_core6.formatMoney)(defaultVariant.price.amount, currency) })
|
|
458
507
|
] }),
|
|
459
508
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
|
|
460
509
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
461
510
|
"button",
|
|
462
511
|
{
|
|
463
512
|
"aria-label": `Decrease ${product.title}`,
|
|
464
|
-
onClick: () => updateQuantity(
|
|
513
|
+
onClick: () => updateQuantity(
|
|
514
|
+
product.id,
|
|
515
|
+
defaultVariant.id,
|
|
516
|
+
selected.quantity - 1
|
|
517
|
+
),
|
|
465
518
|
children: "\u2212"
|
|
466
519
|
}
|
|
467
520
|
),
|
|
@@ -470,7 +523,11 @@ function MixMatchBundle(props) {
|
|
|
470
523
|
"button",
|
|
471
524
|
{
|
|
472
525
|
"aria-label": `Increase ${product.title}`,
|
|
473
|
-
onClick: () => updateQuantity(
|
|
526
|
+
onClick: () => updateQuantity(
|
|
527
|
+
product.id,
|
|
528
|
+
defaultVariant.id,
|
|
529
|
+
selected.quantity + 1
|
|
530
|
+
),
|
|
474
531
|
children: "+"
|
|
475
532
|
}
|
|
476
533
|
)
|
|
@@ -495,9 +552,9 @@ function MixMatchBundle(props) {
|
|
|
495
552
|
{
|
|
496
553
|
className: "lb-bundle__cta",
|
|
497
554
|
onClick: handleAddToCart,
|
|
498
|
-
disabled:
|
|
499
|
-
"aria-busy":
|
|
500
|
-
children:
|
|
555
|
+
disabled: addingToCart || !validation.valid,
|
|
556
|
+
"aria-busy": addingToCart,
|
|
557
|
+
children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${totalQuantity} Items to Cart`
|
|
501
558
|
}
|
|
502
559
|
)
|
|
503
560
|
]
|
|
@@ -506,35 +563,25 @@ function MixMatchBundle(props) {
|
|
|
506
563
|
}
|
|
507
564
|
|
|
508
565
|
// src/components/VolumeBundle.tsx
|
|
509
|
-
var
|
|
510
|
-
var
|
|
566
|
+
var import_react5 = require("react");
|
|
567
|
+
var import_core7 = require("@lime-bundles/core");
|
|
511
568
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
512
569
|
function VolumeBundle(props) {
|
|
513
570
|
const {
|
|
514
571
|
shopDomain,
|
|
515
572
|
storefrontAccessToken,
|
|
516
573
|
bundleGid,
|
|
517
|
-
cartId,
|
|
518
|
-
cartApi,
|
|
519
574
|
appUrl,
|
|
520
575
|
analyticsEnabled,
|
|
521
576
|
onAddToCart,
|
|
522
577
|
onError,
|
|
523
578
|
className
|
|
524
579
|
} = props;
|
|
525
|
-
const
|
|
580
|
+
const result = useBundleData({
|
|
526
581
|
shopDomain,
|
|
527
582
|
storefrontAccessToken,
|
|
528
583
|
bundleGid
|
|
529
584
|
});
|
|
530
|
-
const { addToCart, loading: cartLoading, error: cartError } = useCart({
|
|
531
|
-
shopDomain,
|
|
532
|
-
storefrontAccessToken,
|
|
533
|
-
bundleGid,
|
|
534
|
-
bundleType: "volume",
|
|
535
|
-
cartId,
|
|
536
|
-
cartApi
|
|
537
|
-
});
|
|
538
585
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
539
586
|
shopDomain,
|
|
540
587
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
@@ -542,40 +589,78 @@ function VolumeBundle(props) {
|
|
|
542
589
|
bundleType: "volume",
|
|
543
590
|
enabled: analyticsEnabled !== false
|
|
544
591
|
});
|
|
545
|
-
const [quantity, setQuantity] = (0,
|
|
592
|
+
const [quantity, setQuantity] = (0, import_react5.useState)(1);
|
|
593
|
+
const [addingToCart, setAddingToCart] = (0, import_react5.useState)(false);
|
|
594
|
+
const [cartError, setCartError] = (0, import_react5.useState)(null);
|
|
595
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
|
|
596
|
+
(0, import_react5.useEffect)(() => {
|
|
597
|
+
if (result.status === "error") {
|
|
598
|
+
onError?.(result.error);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (result.status === "success" && result.bundle.bundleType !== "volume") {
|
|
602
|
+
onError?.(
|
|
603
|
+
new Error(
|
|
604
|
+
`VolumeBundle: expected bundleType="volume", got "${result.bundle.bundleType}"`
|
|
605
|
+
)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
}, [result, onError]);
|
|
546
609
|
const product = bundle?.products[0];
|
|
547
610
|
const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
|
|
548
611
|
const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
549
|
-
const tierSavings = bundle ? (0,
|
|
550
|
-
const activeTier = bundle ? (0,
|
|
551
|
-
const handleAddToCart = (0,
|
|
612
|
+
const tierSavings = bundle ? (0, import_core7.calculateTierSavings)(bundle.volumeTiers, basePrice, quantity) : [];
|
|
613
|
+
const activeTier = bundle ? (0, import_core7.getActiveTier)(bundle.volumeTiers, quantity) : null;
|
|
614
|
+
const handleAddToCart = (0, import_react5.useCallback)(async () => {
|
|
552
615
|
if (!bundle || !product) return;
|
|
553
616
|
const variant = product.variants.nodes.find((v) => v.availableForSale);
|
|
554
617
|
if (!variant) return;
|
|
555
|
-
const
|
|
556
|
-
{
|
|
618
|
+
const lines = [
|
|
619
|
+
{
|
|
620
|
+
merchandiseId: variant.id,
|
|
621
|
+
quantity,
|
|
622
|
+
attributes: [
|
|
623
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
624
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
625
|
+
]
|
|
626
|
+
}
|
|
557
627
|
];
|
|
558
|
-
|
|
559
|
-
|
|
628
|
+
setAddingToCart(true);
|
|
629
|
+
setCartError(null);
|
|
630
|
+
try {
|
|
631
|
+
await onAddToCart(lines);
|
|
560
632
|
const unitPrice = activeTier ? basePrice * (1 - activeTier.discountValue / 100) : basePrice;
|
|
561
633
|
trackAddToCart({
|
|
562
634
|
productId: product.id,
|
|
563
635
|
quantity,
|
|
564
636
|
totalPrice: Math.round(unitPrice * quantity * 100) / 100
|
|
565
637
|
});
|
|
566
|
-
|
|
638
|
+
} catch (err) {
|
|
639
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
640
|
+
setCartError(error.message);
|
|
641
|
+
onError?.(error);
|
|
642
|
+
} finally {
|
|
643
|
+
setAddingToCart(false);
|
|
567
644
|
}
|
|
568
|
-
}, [
|
|
569
|
-
|
|
645
|
+
}, [
|
|
646
|
+
bundle,
|
|
647
|
+
product,
|
|
648
|
+
quantity,
|
|
649
|
+
activeTier,
|
|
650
|
+
basePrice,
|
|
651
|
+
onAddToCart,
|
|
652
|
+
onError,
|
|
653
|
+
trackAddToCart
|
|
654
|
+
]);
|
|
655
|
+
if (result.status === "loading") {
|
|
570
656
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
571
657
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
572
658
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "lb-skeleton lb-skeleton--tiers" })
|
|
573
659
|
] });
|
|
574
660
|
}
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
661
|
+
if (result.status === "error") return null;
|
|
662
|
+
if (!bundle) return null;
|
|
663
|
+
if (!product) return null;
|
|
579
664
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
580
665
|
"div",
|
|
581
666
|
{
|
|
@@ -598,35 +683,43 @@ function VolumeBundle(props) {
|
|
|
598
683
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__product-info", children: [
|
|
599
684
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "lb-bundle__product-title", children: product.title }),
|
|
600
685
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "lb-bundle__product-price", children: [
|
|
601
|
-
(0,
|
|
686
|
+
(0, import_core7.formatMoney)(basePrice, currency),
|
|
602
687
|
" each"
|
|
603
688
|
] })
|
|
604
689
|
] })
|
|
605
690
|
] }),
|
|
606
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
691
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
607
692
|
"div",
|
|
608
693
|
{
|
|
609
|
-
className:
|
|
610
|
-
role: "
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
694
|
+
className: "lb-bundle__tiers",
|
|
695
|
+
role: "table",
|
|
696
|
+
"aria-label": "Volume discounts",
|
|
697
|
+
children: tierSavings.map((ts) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
698
|
+
"div",
|
|
699
|
+
{
|
|
700
|
+
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
701
|
+
role: "row",
|
|
702
|
+
children: [
|
|
703
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
|
|
704
|
+
ts.tier.minQuantity,
|
|
705
|
+
"+ items"
|
|
706
|
+
] }),
|
|
707
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-price", role: "cell", children: [
|
|
708
|
+
(0, import_core7.formatMoney)(ts.unitPrice, currency),
|
|
709
|
+
" each"
|
|
710
|
+
] }),
|
|
711
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
|
|
712
|
+
"Save ",
|
|
713
|
+
ts.savingsPercent.toFixed(0),
|
|
714
|
+
"%"
|
|
715
|
+
] }),
|
|
716
|
+
ts.tier.label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__tier-label", role: "cell", children: ts.tier.label })
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
ts.tier.minQuantity
|
|
720
|
+
))
|
|
721
|
+
}
|
|
722
|
+
),
|
|
630
723
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-selector", children: [
|
|
631
724
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
632
725
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "lb-bundle__quantity-control", children: [
|
|
@@ -668,22 +761,28 @@ function VolumeBundle(props) {
|
|
|
668
761
|
{
|
|
669
762
|
className: "lb-bundle__cta",
|
|
670
763
|
onClick: handleAddToCart,
|
|
671
|
-
disabled:
|
|
672
|
-
"aria-busy":
|
|
673
|
-
children:
|
|
764
|
+
disabled: addingToCart,
|
|
765
|
+
"aria-busy": addingToCart,
|
|
766
|
+
children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`
|
|
674
767
|
}
|
|
675
768
|
)
|
|
676
769
|
]
|
|
677
770
|
}
|
|
678
771
|
);
|
|
679
772
|
}
|
|
773
|
+
|
|
774
|
+
// src/index.ts
|
|
775
|
+
var import_core8 = require("@lime-bundles/core");
|
|
680
776
|
// Annotate the CommonJS export names for ESM import in node:
|
|
681
777
|
0 && (module.exports = {
|
|
778
|
+
BundleParseError,
|
|
682
779
|
FixedBundle,
|
|
683
780
|
MixMatchBundle,
|
|
781
|
+
StorefrontApiError,
|
|
684
782
|
VolumeBundle,
|
|
783
|
+
fetchBundleData,
|
|
784
|
+
fetchShopCustomCss,
|
|
685
785
|
useAnalytics,
|
|
686
|
-
useBundleData
|
|
687
|
-
useCart
|
|
786
|
+
useBundleData
|
|
688
787
|
});
|
|
689
788
|
//# sourceMappingURL=index.cjs.map
|