@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.js CHANGED
@@ -1,136 +1,137 @@
1
1
  // src/components/FixedBundle.tsx
2
- import { useCallback as useCallback3 } from "react";
3
- import { formatMoney, calculateDiscount } from "@lime-bundles/core";
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, useRef } from "react";
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
- parseMetaobjectBundle
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 [bundle, setBundle] = useState(null);
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
- abortRef.current = controller;
21
- setLoading(true);
22
- setError(null);
23
- const client = createStorefrontClient({
84
+ setState(INITIAL_STATE);
85
+ fetchBundleData({
24
86
  shopDomain: options.shopDomain,
25
- accessToken: options.storefrontAccessToken
26
- });
27
- client.query(BUNDLE_METAOBJECT_QUERY, {
28
- id: options.bundleGid
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
- if (!data.metaobject) {
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
- setError(err instanceof Error ? err.message : "Failed to load bundle");
50
- setLoading(false);
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
- const addToCart = useCallback(
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 as useMemo2, useRef as useRef2, useCallback as useCallback2, useState as useState3 } from "react";
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 = useRef2(false);
129
- const [element, setElement] = useState3(null);
130
- const elementRef = useCallback2((el) => {
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 = useMemo2(
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 = useCallback2(
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 { bundle, loading, error: loadError } = useBundleData({
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 handleAddToCart = useCallback3(async () => {
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 items = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
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 { variantId: variant.id, quantity: 1 };
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 (items.length === 0) return;
231
- const result = await addToCart(items);
232
- if (result.success) {
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(price, bundle.discountConfig.discountType, bundle.discountConfig.discountValue);
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
- onAddToCart?.(items);
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, addToCart, trackAddToCart, onAddToCart]);
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 (loadError || !bundle) {
252
- if (onError) onError(loadError ?? "Bundle not found");
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: cartLoading,
288
- "aria-busy": cartLoading,
289
- children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? "Add Bundle to Cart"
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 { useState as useState4, useCallback as useCallback4 } from "react";
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 { bundle, loading, error: loadError } = useBundleData({
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(/* @__PURE__ */ new Map());
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 = useCallback4(
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 = useCallback4(
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 = useCallback4(async () => {
404
+ const handleAddToCart = useCallback3(async () => {
374
405
  if (!bundle || !validation.valid) return;
375
- const items = Array.from(selections.values()).map((s) => ({
376
- variantId: s.variantId,
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
- const result = await addToCart(items);
380
- if (result.success) {
381
- const totalPrice = items.reduce((sum, item) => {
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 === item.variantId)
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 === item.variantId
423
+ (v) => v.id === line.merchandiseId
387
424
  );
388
- return sum + parseFloat(variant?.price.amount ?? "0") * item.quantity;
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
- }, [bundle, selections, validation.valid, addToCart, trackAddToCart, totalQuantity, onAddToCart]);
399
- if (loading) {
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 (loadError || !bundle) {
406
- if (onError) onError(loadError ?? "Bundle not found");
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(product.id, defaultVariant.id, selected.quantity - 1),
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(product.id, defaultVariant.id, selected.quantity + 1),
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: cartLoading || !validation.valid,
484
- "aria-busy": cartLoading,
485
- children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${totalQuantity} Items to Cart`
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 { useState as useState5, useCallback as useCallback5 } from "react";
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 { bundle, loading, error: loadError } = useBundleData({
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 = useCallback5(async () => {
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 items = [
545
- { variantId: variant.id, quantity }
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
- const result = await addToCart(items);
548
- if (result.success) {
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
- onAddToCart?.(items);
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
- }, [bundle, product, quantity, activeTier, basePrice, addToCart, trackAddToCart, onAddToCart]);
558
- if (loading) {
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 (loadError || !bundle || !product) {
565
- if (onError) onError(loadError ?? "Bundle not found");
566
- return null;
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("div", { className: "lb-bundle__tiers", role: "table", "aria-label": "Volume discounts", children: tierSavings.map((ts) => /* @__PURE__ */ jsxs3(
679
+ /* @__PURE__ */ jsx3(
596
680
  "div",
597
681
  {
598
- className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
599
- role: "row",
600
- children: [
601
- /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
602
- ts.tier.minQuantity,
603
- "+ items"
604
- ] }),
605
- /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-price", role: "cell", children: [
606
- formatMoney3(ts.unitPrice, currency),
607
- " each"
608
- ] }),
609
- /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
610
- "Save ",
611
- ts.savingsPercent.toFixed(0),
612
- "%"
613
- ] }),
614
- ts.tier.label && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__tier-label", role: "cell", children: ts.tier.label })
615
- ]
616
- },
617
- ts.tier.minQuantity
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: cartLoading,
661
- "aria-busy": cartLoading,
662
- children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`
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