@lime-bundles/react 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.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.js
CHANGED
|
@@ -1,136 +1,137 @@
|
|
|
1
1
|
// src/components/FixedBundle.tsx
|
|
2
|
-
import { useCallback as
|
|
3
|
-
import {
|
|
2
|
+
import { useCallback as useCallback2, useEffect as useEffect3, useState as useState3 } from "react";
|
|
3
|
+
import {
|
|
4
|
+
formatMoney,
|
|
5
|
+
calculateDiscount
|
|
6
|
+
} from "@lime-bundles/core";
|
|
4
7
|
|
|
5
8
|
// src/hooks/useBundleData.ts
|
|
6
|
-
import { useState, useEffect
|
|
9
|
+
import { useState, useEffect } from "react";
|
|
10
|
+
import { injectCustomCss } from "@lime-bundles/core";
|
|
11
|
+
|
|
12
|
+
// src/fetchBundleData.ts
|
|
7
13
|
import {
|
|
8
14
|
createStorefrontClient,
|
|
9
15
|
BUNDLE_METAOBJECT_QUERY,
|
|
10
|
-
|
|
16
|
+
parseMetaobjectBundleStrict,
|
|
17
|
+
BundleParseError
|
|
11
18
|
} from "@lime-bundles/core";
|
|
19
|
+
async function fetchBundleData(options) {
|
|
20
|
+
const client = createStorefrontClient({
|
|
21
|
+
shopDomain: options.shopDomain,
|
|
22
|
+
accessToken: options.storefrontAccessToken,
|
|
23
|
+
buyerIp: options.buyerIp,
|
|
24
|
+
apiVersion: options.apiVersion
|
|
25
|
+
});
|
|
26
|
+
const data = await client.query(
|
|
27
|
+
BUNDLE_METAOBJECT_QUERY,
|
|
28
|
+
{ id: options.bundleGid },
|
|
29
|
+
{ signal: options.signal }
|
|
30
|
+
);
|
|
31
|
+
if (!data.metaobject) {
|
|
32
|
+
throw new BundleParseError(
|
|
33
|
+
`Bundle not found: ${options.bundleGid}`,
|
|
34
|
+
"not_found"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return parseMetaobjectBundleStrict(
|
|
38
|
+
data.metaobject.id,
|
|
39
|
+
data.metaobject.fields
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/fetchShopCustomCss.ts
|
|
44
|
+
import {
|
|
45
|
+
createStorefrontClient as createStorefrontClient2,
|
|
46
|
+
SHOP_CUSTOM_CSS_QUERY
|
|
47
|
+
} from "@lime-bundles/core";
|
|
48
|
+
var cache = /* @__PURE__ */ new Map();
|
|
49
|
+
async function fetchShopCustomCss(options) {
|
|
50
|
+
const cached = cache.get(options.shopDomain);
|
|
51
|
+
if (cached) return cached;
|
|
52
|
+
const promise = (async () => {
|
|
53
|
+
const client = createStorefrontClient2({
|
|
54
|
+
shopDomain: options.shopDomain,
|
|
55
|
+
accessToken: options.storefrontAccessToken,
|
|
56
|
+
buyerIp: options.buyerIp
|
|
57
|
+
});
|
|
58
|
+
const data = await client.query(
|
|
59
|
+
SHOP_CUSTOM_CSS_QUERY,
|
|
60
|
+
void 0,
|
|
61
|
+
{ signal: options.signal }
|
|
62
|
+
);
|
|
63
|
+
return data.shop?.metafield?.value ?? null;
|
|
64
|
+
})();
|
|
65
|
+
cache.set(options.shopDomain, promise);
|
|
66
|
+
promise.catch(() => {
|
|
67
|
+
if (cache.get(options.shopDomain) === promise) {
|
|
68
|
+
cache.delete(options.shopDomain);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return promise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/hooks/useBundleData.ts
|
|
75
|
+
var INITIAL_STATE = {
|
|
76
|
+
status: "loading",
|
|
77
|
+
bundle: null,
|
|
78
|
+
error: null
|
|
79
|
+
};
|
|
12
80
|
function useBundleData(options) {
|
|
13
|
-
const [
|
|
14
|
-
const [loading, setLoading] = useState(true);
|
|
15
|
-
const [error, setError] = useState(null);
|
|
16
|
-
const abortRef = useRef(null);
|
|
81
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
17
82
|
useEffect(() => {
|
|
18
|
-
abortRef.current?.abort();
|
|
19
83
|
const controller = new AbortController();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
setError(null);
|
|
23
|
-
const client = createStorefrontClient({
|
|
84
|
+
setState(INITIAL_STATE);
|
|
85
|
+
fetchBundleData({
|
|
24
86
|
shopDomain: options.shopDomain,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}).then((data) => {
|
|
87
|
+
storefrontAccessToken: options.storefrontAccessToken,
|
|
88
|
+
bundleGid: options.bundleGid,
|
|
89
|
+
signal: controller.signal
|
|
90
|
+
}).then((bundle) => {
|
|
30
91
|
if (controller.signal.aborted) return;
|
|
31
|
-
|
|
32
|
-
setError("Bundle not found");
|
|
33
|
-
setBundle(null);
|
|
34
|
-
} else {
|
|
35
|
-
const parsed = parseMetaobjectBundle(
|
|
36
|
-
data.metaobject.id,
|
|
37
|
-
data.metaobject.fields
|
|
38
|
-
);
|
|
39
|
-
if (parsed) {
|
|
40
|
-
setBundle(parsed);
|
|
41
|
-
} else {
|
|
42
|
-
setError("Bundle is not active or has expired");
|
|
43
|
-
setBundle(null);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
setLoading(false);
|
|
92
|
+
setState({ status: "success", bundle, error: null });
|
|
47
93
|
}).catch((err) => {
|
|
48
94
|
if (controller.signal.aborted) return;
|
|
49
|
-
|
|
50
|
-
|
|
95
|
+
setState({
|
|
96
|
+
status: "error",
|
|
97
|
+
bundle: null,
|
|
98
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
fetchShopCustomCss({
|
|
102
|
+
shopDomain: options.shopDomain,
|
|
103
|
+
storefrontAccessToken: options.storefrontAccessToken,
|
|
104
|
+
signal: controller.signal
|
|
105
|
+
}).then((css) => {
|
|
106
|
+
if (controller.signal.aborted) return;
|
|
107
|
+
injectCustomCss(options.shopDomain, css);
|
|
108
|
+
}).catch(() => {
|
|
51
109
|
});
|
|
52
110
|
return () => {
|
|
53
111
|
controller.abort();
|
|
54
112
|
};
|
|
55
|
-
}, [options.shopDomain, options.storefrontAccessToken, options.bundleGid]);
|
|
56
|
-
return { bundle, loading, error };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/hooks/useCart.ts
|
|
60
|
-
import { useState as useState2, useCallback, useMemo } from "react";
|
|
61
|
-
import {
|
|
62
|
-
detectCartApi,
|
|
63
|
-
createAjaxCartApi,
|
|
64
|
-
createStorefrontCartApi,
|
|
65
|
-
createStorefrontClient as createStorefrontClient2
|
|
66
|
-
} from "@lime-bundles/core";
|
|
67
|
-
function useCart(options) {
|
|
68
|
-
const [loading, setLoading] = useState2(false);
|
|
69
|
-
const [error, setError] = useState2(null);
|
|
70
|
-
const [lastResult, setLastResult] = useState2(null);
|
|
71
|
-
const cart = useMemo(() => {
|
|
72
|
-
const apiType = options.cartApi ?? detectCartApi();
|
|
73
|
-
if (apiType === "ajax") {
|
|
74
|
-
return createAjaxCartApi(options.bundleGid, options.bundleType);
|
|
75
|
-
}
|
|
76
|
-
const client = createStorefrontClient2({
|
|
77
|
-
shopDomain: options.shopDomain,
|
|
78
|
-
accessToken: options.storefrontAccessToken
|
|
79
|
-
});
|
|
80
|
-
return createStorefrontCartApi(
|
|
81
|
-
client,
|
|
82
|
-
options.bundleGid,
|
|
83
|
-
options.bundleType,
|
|
84
|
-
options.cartId
|
|
85
|
-
);
|
|
86
113
|
}, [
|
|
87
114
|
options.shopDomain,
|
|
88
115
|
options.storefrontAccessToken,
|
|
89
|
-
options.bundleGid
|
|
90
|
-
options.bundleType,
|
|
91
|
-
options.cartId,
|
|
92
|
-
options.cartApi
|
|
116
|
+
options.bundleGid
|
|
93
117
|
]);
|
|
94
|
-
|
|
95
|
-
async (items) => {
|
|
96
|
-
setLoading(true);
|
|
97
|
-
setError(null);
|
|
98
|
-
try {
|
|
99
|
-
const result = await cart.addLines(items);
|
|
100
|
-
setLastResult(result);
|
|
101
|
-
if (!result.success) {
|
|
102
|
-
setError(result.error ?? "Failed to add to cart");
|
|
103
|
-
}
|
|
104
|
-
return result;
|
|
105
|
-
} catch (err) {
|
|
106
|
-
const message = err instanceof Error ? err.message : "Failed to add to cart";
|
|
107
|
-
setError(message);
|
|
108
|
-
const result = { success: false, error: message };
|
|
109
|
-
setLastResult(result);
|
|
110
|
-
return result;
|
|
111
|
-
} finally {
|
|
112
|
-
setLoading(false);
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
[cart]
|
|
116
|
-
);
|
|
117
|
-
return { addToCart, loading, error, lastResult };
|
|
118
|
+
return state;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
// src/hooks/useAnalytics.ts
|
|
121
|
-
import { useEffect as useEffect2, useMemo
|
|
122
|
+
import { useEffect as useEffect2, useMemo, useRef, useCallback, useState as useState2 } from "react";
|
|
122
123
|
import {
|
|
123
124
|
reportImpression,
|
|
124
125
|
reportAddToCart,
|
|
125
126
|
observeImpression
|
|
126
127
|
} from "@lime-bundles/core";
|
|
127
128
|
function useAnalytics(options) {
|
|
128
|
-
const impressionFiredRef =
|
|
129
|
-
const [element, setElement] =
|
|
130
|
-
const elementRef =
|
|
129
|
+
const impressionFiredRef = useRef(false);
|
|
130
|
+
const [element, setElement] = useState2(null);
|
|
131
|
+
const elementRef = useCallback((el) => {
|
|
131
132
|
setElement(el);
|
|
132
133
|
}, []);
|
|
133
|
-
const config =
|
|
134
|
+
const config = useMemo(
|
|
134
135
|
() => ({ shopDomain: options.shopDomain, appUrl: options.appUrl }),
|
|
135
136
|
[options.shopDomain, options.appUrl]
|
|
136
137
|
);
|
|
@@ -161,7 +162,7 @@ function useAnalytics(options) {
|
|
|
161
162
|
options.abTestId,
|
|
162
163
|
options.abVariant
|
|
163
164
|
]);
|
|
164
|
-
const trackAddToCart =
|
|
165
|
+
const trackAddToCart = useCallback(
|
|
165
166
|
(event) => {
|
|
166
167
|
if (options.enabled === false) return;
|
|
167
168
|
reportAddToCart(config, {
|
|
@@ -193,27 +194,17 @@ function FixedBundle(props) {
|
|
|
193
194
|
shopDomain,
|
|
194
195
|
storefrontAccessToken,
|
|
195
196
|
bundleGid,
|
|
196
|
-
cartId,
|
|
197
|
-
cartApi,
|
|
198
197
|
appUrl,
|
|
199
198
|
analyticsEnabled,
|
|
200
199
|
onAddToCart,
|
|
201
200
|
onError,
|
|
202
201
|
className
|
|
203
202
|
} = props;
|
|
204
|
-
const
|
|
203
|
+
const result = useBundleData({
|
|
205
204
|
shopDomain,
|
|
206
205
|
storefrontAccessToken,
|
|
207
206
|
bundleGid
|
|
208
207
|
});
|
|
209
|
-
const { addToCart, loading: cartLoading, error: cartError } = useCart({
|
|
210
|
-
shopDomain,
|
|
211
|
-
storefrontAccessToken,
|
|
212
|
-
bundleGid,
|
|
213
|
-
bundleType: "fixed",
|
|
214
|
-
cartId,
|
|
215
|
-
cartApi
|
|
216
|
-
});
|
|
217
208
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
218
209
|
shopDomain,
|
|
219
210
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
@@ -221,37 +212,69 @@ function FixedBundle(props) {
|
|
|
221
212
|
bundleType: "fixed",
|
|
222
213
|
enabled: analyticsEnabled !== false
|
|
223
214
|
});
|
|
224
|
-
const
|
|
215
|
+
const [addingToCart, setAddingToCart] = useState3(false);
|
|
216
|
+
const [cartError, setCartError] = useState3(null);
|
|
217
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "fixed" ? result.bundle : null;
|
|
218
|
+
useEffect3(() => {
|
|
219
|
+
if (result.status === "error") {
|
|
220
|
+
onError?.(result.error);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (result.status === "success" && result.bundle.bundleType !== "fixed") {
|
|
224
|
+
onError?.(
|
|
225
|
+
new Error(
|
|
226
|
+
`FixedBundle: expected bundleType="fixed", got "${result.bundle.bundleType}"`
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}, [result, onError]);
|
|
231
|
+
const handleAddToCart = useCallback2(async () => {
|
|
225
232
|
if (!bundle) return;
|
|
226
|
-
const
|
|
233
|
+
const lines = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
|
|
227
234
|
const variant = p.variants.nodes.find((v) => v.availableForSale);
|
|
228
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
merchandiseId: variant.id,
|
|
237
|
+
quantity: 1,
|
|
238
|
+
attributes: [
|
|
239
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
240
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
241
|
+
]
|
|
242
|
+
};
|
|
229
243
|
});
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
if (lines.length === 0) return;
|
|
245
|
+
setAddingToCart(true);
|
|
246
|
+
setCartError(null);
|
|
247
|
+
try {
|
|
248
|
+
await onAddToCart(lines);
|
|
233
249
|
const totalPrice = bundle.products.reduce((sum, p) => {
|
|
234
250
|
const price = parseFloat(p.priceRange.minVariantPrice.amount);
|
|
235
|
-
return sum + calculateDiscount(
|
|
251
|
+
return sum + calculateDiscount(
|
|
252
|
+
price,
|
|
253
|
+
bundle.discountConfig.discountType,
|
|
254
|
+
bundle.discountConfig.discountValue
|
|
255
|
+
);
|
|
236
256
|
}, 0);
|
|
237
257
|
trackAddToCart({
|
|
238
258
|
productId: bundle.products[0]?.id ?? "",
|
|
239
259
|
quantity: 1,
|
|
240
260
|
totalPrice: Math.round(totalPrice * 100) / 100
|
|
241
261
|
});
|
|
242
|
-
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
264
|
+
setCartError(error.message);
|
|
265
|
+
onError?.(error);
|
|
266
|
+
} finally {
|
|
267
|
+
setAddingToCart(false);
|
|
243
268
|
}
|
|
244
|
-
}, [bundle,
|
|
245
|
-
if (loading) {
|
|
269
|
+
}, [bundle, onAddToCart, onError, trackAddToCart]);
|
|
270
|
+
if (result.status === "loading") {
|
|
246
271
|
return /* @__PURE__ */ jsxs("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
247
272
|
/* @__PURE__ */ jsx("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
248
273
|
/* @__PURE__ */ jsx("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
249
274
|
] });
|
|
250
275
|
}
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
276
|
+
if (result.status === "error") return null;
|
|
277
|
+
if (!bundle) return null;
|
|
255
278
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
256
279
|
return /* @__PURE__ */ jsxs(
|
|
257
280
|
"div",
|
|
@@ -284,9 +307,9 @@ function FixedBundle(props) {
|
|
|
284
307
|
{
|
|
285
308
|
className: "lb-bundle__cta",
|
|
286
309
|
onClick: handleAddToCart,
|
|
287
|
-
disabled:
|
|
288
|
-
"aria-busy":
|
|
289
|
-
children:
|
|
310
|
+
disabled: addingToCart,
|
|
311
|
+
"aria-busy": addingToCart,
|
|
312
|
+
children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? "Add Bundle to Cart"
|
|
290
313
|
}
|
|
291
314
|
)
|
|
292
315
|
]
|
|
@@ -295,7 +318,7 @@ function FixedBundle(props) {
|
|
|
295
318
|
}
|
|
296
319
|
|
|
297
320
|
// src/components/MixMatchBundle.tsx
|
|
298
|
-
import {
|
|
321
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useState as useState4 } from "react";
|
|
299
322
|
import {
|
|
300
323
|
formatMoney as formatMoney2,
|
|
301
324
|
validateQuantity
|
|
@@ -306,27 +329,17 @@ function MixMatchBundle(props) {
|
|
|
306
329
|
shopDomain,
|
|
307
330
|
storefrontAccessToken,
|
|
308
331
|
bundleGid,
|
|
309
|
-
cartId,
|
|
310
|
-
cartApi,
|
|
311
332
|
appUrl,
|
|
312
333
|
analyticsEnabled,
|
|
313
334
|
onAddToCart,
|
|
314
335
|
onError,
|
|
315
336
|
className
|
|
316
337
|
} = props;
|
|
317
|
-
const
|
|
338
|
+
const result = useBundleData({
|
|
318
339
|
shopDomain,
|
|
319
340
|
storefrontAccessToken,
|
|
320
341
|
bundleGid
|
|
321
342
|
});
|
|
322
|
-
const { addToCart, loading: cartLoading, error: cartError } = useCart({
|
|
323
|
-
shopDomain,
|
|
324
|
-
storefrontAccessToken,
|
|
325
|
-
bundleGid,
|
|
326
|
-
bundleType: "mix_match",
|
|
327
|
-
cartId,
|
|
328
|
-
cartApi
|
|
329
|
-
});
|
|
330
343
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
331
344
|
shopDomain,
|
|
332
345
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
@@ -334,13 +347,31 @@ function MixMatchBundle(props) {
|
|
|
334
347
|
bundleType: "mix_match",
|
|
335
348
|
enabled: analyticsEnabled !== false
|
|
336
349
|
});
|
|
337
|
-
const [selections, setSelections] = useState4(
|
|
350
|
+
const [selections, setSelections] = useState4(
|
|
351
|
+
/* @__PURE__ */ new Map()
|
|
352
|
+
);
|
|
353
|
+
const [addingToCart, setAddingToCart] = useState4(false);
|
|
354
|
+
const [cartError, setCartError] = useState4(null);
|
|
355
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "mix_match" ? result.bundle : null;
|
|
356
|
+
useEffect4(() => {
|
|
357
|
+
if (result.status === "error") {
|
|
358
|
+
onError?.(result.error);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (result.status === "success" && result.bundle.bundleType !== "mix_match") {
|
|
362
|
+
onError?.(
|
|
363
|
+
new Error(
|
|
364
|
+
`MixMatchBundle: expected bundleType="mix_match", got "${result.bundle.bundleType}"`
|
|
365
|
+
)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}, [result, onError]);
|
|
338
369
|
const totalQuantity = Array.from(selections.values()).reduce(
|
|
339
370
|
(sum, s) => sum + s.quantity,
|
|
340
371
|
0
|
|
341
372
|
);
|
|
342
373
|
const validation = bundle ? validateQuantity(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
|
|
343
|
-
const toggleProduct =
|
|
374
|
+
const toggleProduct = useCallback3(
|
|
344
375
|
(productId, variant) => {
|
|
345
376
|
setSelections((prev) => {
|
|
346
377
|
const next = new Map(prev);
|
|
@@ -355,7 +386,7 @@ function MixMatchBundle(props) {
|
|
|
355
386
|
},
|
|
356
387
|
[]
|
|
357
388
|
);
|
|
358
|
-
const updateQuantity =
|
|
389
|
+
const updateQuantity = useCallback3(
|
|
359
390
|
(productId, variantId, quantity) => {
|
|
360
391
|
setSelections((prev) => {
|
|
361
392
|
const next = new Map(prev);
|
|
@@ -370,42 +401,59 @@ function MixMatchBundle(props) {
|
|
|
370
401
|
},
|
|
371
402
|
[]
|
|
372
403
|
);
|
|
373
|
-
const handleAddToCart =
|
|
404
|
+
const handleAddToCart = useCallback3(async () => {
|
|
374
405
|
if (!bundle || !validation.valid) return;
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
quantity: s.quantity
|
|
406
|
+
const lines = Array.from(selections.values()).map((s) => ({
|
|
407
|
+
merchandiseId: s.variantId,
|
|
408
|
+
quantity: s.quantity,
|
|
409
|
+
attributes: [
|
|
410
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
411
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
412
|
+
]
|
|
378
413
|
}));
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
414
|
+
setAddingToCart(true);
|
|
415
|
+
setCartError(null);
|
|
416
|
+
try {
|
|
417
|
+
await onAddToCart(lines);
|
|
418
|
+
const totalPrice = lines.reduce((sum, line) => {
|
|
382
419
|
const product = bundle.products.find(
|
|
383
|
-
(p) => p.variants.nodes.some((v) => v.id ===
|
|
420
|
+
(p) => p.variants.nodes.some((v) => v.id === line.merchandiseId)
|
|
384
421
|
);
|
|
385
422
|
const variant = product?.variants.nodes.find(
|
|
386
|
-
(v) => v.id ===
|
|
423
|
+
(v) => v.id === line.merchandiseId
|
|
387
424
|
);
|
|
388
|
-
return sum + parseFloat(variant?.price.amount ?? "0") *
|
|
425
|
+
return sum + parseFloat(variant?.price.amount ?? "0") * line.quantity;
|
|
389
426
|
}, 0);
|
|
390
427
|
trackAddToCart({
|
|
391
428
|
productId: bundle.products[0]?.id ?? "",
|
|
392
429
|
quantity: totalQuantity,
|
|
393
430
|
totalPrice: Math.round(totalPrice * 100) / 100
|
|
394
431
|
});
|
|
395
|
-
onAddToCart?.(items);
|
|
396
432
|
setSelections(/* @__PURE__ */ new Map());
|
|
433
|
+
} catch (err) {
|
|
434
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
435
|
+
setCartError(error.message);
|
|
436
|
+
onError?.(error);
|
|
437
|
+
} finally {
|
|
438
|
+
setAddingToCart(false);
|
|
397
439
|
}
|
|
398
|
-
}, [
|
|
399
|
-
|
|
440
|
+
}, [
|
|
441
|
+
bundle,
|
|
442
|
+
selections,
|
|
443
|
+
validation.valid,
|
|
444
|
+
onAddToCart,
|
|
445
|
+
onError,
|
|
446
|
+
trackAddToCart,
|
|
447
|
+
totalQuantity
|
|
448
|
+
]);
|
|
449
|
+
if (result.status === "loading") {
|
|
400
450
|
return /* @__PURE__ */ jsxs2("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
401
451
|
/* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
402
452
|
/* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--products" })
|
|
403
453
|
] });
|
|
404
454
|
}
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
455
|
+
if (result.status === "error") return null;
|
|
456
|
+
if (!bundle) return null;
|
|
409
457
|
const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
410
458
|
return /* @__PURE__ */ jsxs2(
|
|
411
459
|
"div",
|
|
@@ -446,7 +494,11 @@ function MixMatchBundle(props) {
|
|
|
446
494
|
"button",
|
|
447
495
|
{
|
|
448
496
|
"aria-label": `Decrease ${product.title}`,
|
|
449
|
-
onClick: () => updateQuantity(
|
|
497
|
+
onClick: () => updateQuantity(
|
|
498
|
+
product.id,
|
|
499
|
+
defaultVariant.id,
|
|
500
|
+
selected.quantity - 1
|
|
501
|
+
),
|
|
450
502
|
children: "\u2212"
|
|
451
503
|
}
|
|
452
504
|
),
|
|
@@ -455,7 +507,11 @@ function MixMatchBundle(props) {
|
|
|
455
507
|
"button",
|
|
456
508
|
{
|
|
457
509
|
"aria-label": `Increase ${product.title}`,
|
|
458
|
-
onClick: () => updateQuantity(
|
|
510
|
+
onClick: () => updateQuantity(
|
|
511
|
+
product.id,
|
|
512
|
+
defaultVariant.id,
|
|
513
|
+
selected.quantity + 1
|
|
514
|
+
),
|
|
459
515
|
children: "+"
|
|
460
516
|
}
|
|
461
517
|
)
|
|
@@ -480,9 +536,9 @@ function MixMatchBundle(props) {
|
|
|
480
536
|
{
|
|
481
537
|
className: "lb-bundle__cta",
|
|
482
538
|
onClick: handleAddToCart,
|
|
483
|
-
disabled:
|
|
484
|
-
"aria-busy":
|
|
485
|
-
children:
|
|
539
|
+
disabled: addingToCart || !validation.valid,
|
|
540
|
+
"aria-busy": addingToCart,
|
|
541
|
+
children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${totalQuantity} Items to Cart`
|
|
486
542
|
}
|
|
487
543
|
)
|
|
488
544
|
]
|
|
@@ -491,7 +547,7 @@ function MixMatchBundle(props) {
|
|
|
491
547
|
}
|
|
492
548
|
|
|
493
549
|
// src/components/VolumeBundle.tsx
|
|
494
|
-
import {
|
|
550
|
+
import { useCallback as useCallback4, useEffect as useEffect5, useState as useState5 } from "react";
|
|
495
551
|
import {
|
|
496
552
|
formatMoney as formatMoney3,
|
|
497
553
|
calculateTierSavings,
|
|
@@ -503,27 +559,17 @@ function VolumeBundle(props) {
|
|
|
503
559
|
shopDomain,
|
|
504
560
|
storefrontAccessToken,
|
|
505
561
|
bundleGid,
|
|
506
|
-
cartId,
|
|
507
|
-
cartApi,
|
|
508
562
|
appUrl,
|
|
509
563
|
analyticsEnabled,
|
|
510
564
|
onAddToCart,
|
|
511
565
|
onError,
|
|
512
566
|
className
|
|
513
567
|
} = props;
|
|
514
|
-
const
|
|
568
|
+
const result = useBundleData({
|
|
515
569
|
shopDomain,
|
|
516
570
|
storefrontAccessToken,
|
|
517
571
|
bundleGid
|
|
518
572
|
});
|
|
519
|
-
const { addToCart, loading: cartLoading, error: cartError } = useCart({
|
|
520
|
-
shopDomain,
|
|
521
|
-
storefrontAccessToken,
|
|
522
|
-
bundleGid,
|
|
523
|
-
bundleType: "volume",
|
|
524
|
-
cartId,
|
|
525
|
-
cartApi
|
|
526
|
-
});
|
|
527
573
|
const { elementRef, trackAddToCart } = useAnalytics({
|
|
528
574
|
shopDomain,
|
|
529
575
|
appUrl: appUrl ?? `https://${shopDomain}`,
|
|
@@ -532,39 +578,77 @@ function VolumeBundle(props) {
|
|
|
532
578
|
enabled: analyticsEnabled !== false
|
|
533
579
|
});
|
|
534
580
|
const [quantity, setQuantity] = useState5(1);
|
|
581
|
+
const [addingToCart, setAddingToCart] = useState5(false);
|
|
582
|
+
const [cartError, setCartError] = useState5(null);
|
|
583
|
+
const bundle = result.status === "success" && result.bundle.bundleType === "volume" ? result.bundle : null;
|
|
584
|
+
useEffect5(() => {
|
|
585
|
+
if (result.status === "error") {
|
|
586
|
+
onError?.(result.error);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (result.status === "success" && result.bundle.bundleType !== "volume") {
|
|
590
|
+
onError?.(
|
|
591
|
+
new Error(
|
|
592
|
+
`VolumeBundle: expected bundleType="volume", got "${result.bundle.bundleType}"`
|
|
593
|
+
)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}, [result, onError]);
|
|
535
597
|
const product = bundle?.products[0];
|
|
536
598
|
const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
|
|
537
599
|
const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
|
|
538
600
|
const tierSavings = bundle ? calculateTierSavings(bundle.volumeTiers, basePrice, quantity) : [];
|
|
539
601
|
const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
|
|
540
|
-
const handleAddToCart =
|
|
602
|
+
const handleAddToCart = useCallback4(async () => {
|
|
541
603
|
if (!bundle || !product) return;
|
|
542
604
|
const variant = product.variants.nodes.find((v) => v.availableForSale);
|
|
543
605
|
if (!variant) return;
|
|
544
|
-
const
|
|
545
|
-
{
|
|
606
|
+
const lines = [
|
|
607
|
+
{
|
|
608
|
+
merchandiseId: variant.id,
|
|
609
|
+
quantity,
|
|
610
|
+
attributes: [
|
|
611
|
+
{ key: "_lime_bundle_gid", value: bundle.id },
|
|
612
|
+
{ key: "_lime_bundle_type", value: bundle.bundleType }
|
|
613
|
+
]
|
|
614
|
+
}
|
|
546
615
|
];
|
|
547
|
-
|
|
548
|
-
|
|
616
|
+
setAddingToCart(true);
|
|
617
|
+
setCartError(null);
|
|
618
|
+
try {
|
|
619
|
+
await onAddToCart(lines);
|
|
549
620
|
const unitPrice = activeTier ? basePrice * (1 - activeTier.discountValue / 100) : basePrice;
|
|
550
621
|
trackAddToCart({
|
|
551
622
|
productId: product.id,
|
|
552
623
|
quantity,
|
|
553
624
|
totalPrice: Math.round(unitPrice * quantity * 100) / 100
|
|
554
625
|
});
|
|
555
|
-
|
|
626
|
+
} catch (err) {
|
|
627
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
628
|
+
setCartError(error.message);
|
|
629
|
+
onError?.(error);
|
|
630
|
+
} finally {
|
|
631
|
+
setAddingToCart(false);
|
|
556
632
|
}
|
|
557
|
-
}, [
|
|
558
|
-
|
|
633
|
+
}, [
|
|
634
|
+
bundle,
|
|
635
|
+
product,
|
|
636
|
+
quantity,
|
|
637
|
+
activeTier,
|
|
638
|
+
basePrice,
|
|
639
|
+
onAddToCart,
|
|
640
|
+
onError,
|
|
641
|
+
trackAddToCart
|
|
642
|
+
]);
|
|
643
|
+
if (result.status === "loading") {
|
|
559
644
|
return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
|
|
560
645
|
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
|
|
561
646
|
/* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--tiers" })
|
|
562
647
|
] });
|
|
563
648
|
}
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
649
|
+
if (result.status === "error") return null;
|
|
650
|
+
if (!bundle) return null;
|
|
651
|
+
if (!product) return null;
|
|
568
652
|
return /* @__PURE__ */ jsxs3(
|
|
569
653
|
"div",
|
|
570
654
|
{
|
|
@@ -592,30 +676,38 @@ function VolumeBundle(props) {
|
|
|
592
676
|
] })
|
|
593
677
|
] })
|
|
594
678
|
] }),
|
|
595
|
-
/* @__PURE__ */ jsx3(
|
|
679
|
+
/* @__PURE__ */ jsx3(
|
|
596
680
|
"div",
|
|
597
681
|
{
|
|
598
|
-
className:
|
|
599
|
-
role: "
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
682
|
+
className: "lb-bundle__tiers",
|
|
683
|
+
role: "table",
|
|
684
|
+
"aria-label": "Volume discounts",
|
|
685
|
+
children: tierSavings.map((ts) => /* @__PURE__ */ jsxs3(
|
|
686
|
+
"div",
|
|
687
|
+
{
|
|
688
|
+
className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
|
|
689
|
+
role: "row",
|
|
690
|
+
children: [
|
|
691
|
+
/* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
|
|
692
|
+
ts.tier.minQuantity,
|
|
693
|
+
"+ items"
|
|
694
|
+
] }),
|
|
695
|
+
/* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-price", role: "cell", children: [
|
|
696
|
+
formatMoney3(ts.unitPrice, currency),
|
|
697
|
+
" each"
|
|
698
|
+
] }),
|
|
699
|
+
/* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
|
|
700
|
+
"Save ",
|
|
701
|
+
ts.savingsPercent.toFixed(0),
|
|
702
|
+
"%"
|
|
703
|
+
] }),
|
|
704
|
+
ts.tier.label && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__tier-label", role: "cell", children: ts.tier.label })
|
|
705
|
+
]
|
|
706
|
+
},
|
|
707
|
+
ts.tier.minQuantity
|
|
708
|
+
))
|
|
709
|
+
}
|
|
710
|
+
),
|
|
619
711
|
/* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-selector", children: [
|
|
620
712
|
/* @__PURE__ */ jsx3("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
|
|
621
713
|
/* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
|
|
@@ -657,21 +749,27 @@ function VolumeBundle(props) {
|
|
|
657
749
|
{
|
|
658
750
|
className: "lb-bundle__cta",
|
|
659
751
|
onClick: handleAddToCart,
|
|
660
|
-
disabled:
|
|
661
|
-
"aria-busy":
|
|
662
|
-
children:
|
|
752
|
+
disabled: addingToCart,
|
|
753
|
+
"aria-busy": addingToCart,
|
|
754
|
+
children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`
|
|
663
755
|
}
|
|
664
756
|
)
|
|
665
757
|
]
|
|
666
758
|
}
|
|
667
759
|
);
|
|
668
760
|
}
|
|
761
|
+
|
|
762
|
+
// src/index.ts
|
|
763
|
+
import { BundleParseError as BundleParseError2, StorefrontApiError } from "@lime-bundles/core";
|
|
669
764
|
export {
|
|
765
|
+
BundleParseError2 as BundleParseError,
|
|
670
766
|
FixedBundle,
|
|
671
767
|
MixMatchBundle,
|
|
768
|
+
StorefrontApiError,
|
|
672
769
|
VolumeBundle,
|
|
770
|
+
fetchBundleData,
|
|
771
|
+
fetchShopCustomCss,
|
|
673
772
|
useAnalytics,
|
|
674
|
-
useBundleData
|
|
675
|
-
useCart
|
|
773
|
+
useBundleData
|
|
676
774
|
};
|
|
677
775
|
//# sourceMappingURL=index.js.map
|