@lime-bundles/react 1.0.0 → 2.1.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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # @lime-bundles/react
2
2
 
3
- React components and hooks for Lime Bundles on Hydrogen, Next.js, Vite, Remix, or any React renderer. BYO-cart: you pass an `onAddToCart` callback; Lime Bundles renders the UI and fires analytics.
3
+ React components and hooks for the **Lime Bundles Shopify app**. Use on Hydrogen, Next.js, Vite, Remix, or any React renderer. If you're not a merchant using Lime Bundles, this package probably isn't what you're looking for.
4
+
5
+ Bring your own cart via an `onAddToCart` callback; the SDK handles data fetching, rendering, and analytics.
4
6
 
5
7
  ## Install
6
8
 
@@ -10,10 +12,10 @@ npm install @lime-bundles/react @lime-bundles/core
10
12
 
11
13
  Peer dependencies: `react >=18`, `react-dom >=18`.
12
14
 
13
- ## Components
15
+ ## Drop-in components
14
16
 
15
17
  ```tsx
16
- import { FixedBundle, VolumeBundle, MixMatchBundle } from "@lime-bundles/react";
18
+ import { FixedBundle } from "@lime-bundles/react";
17
19
 
18
20
  <FixedBundle
19
21
  shopDomain="my-shop.myshopify.com"
@@ -26,107 +28,99 @@ import { FixedBundle, VolumeBundle, MixMatchBundle } from "@lime-bundles/react";
26
28
  />
27
29
  ```
28
30
 
29
- All three components share the same `BundleComponentProps` shape:
31
+ Same props for `<VolumeBundle>` and `<MixMatchBundle>`.
32
+
33
+ ### Props: `BundleComponentProps`
30
34
 
31
35
  | Prop | Type | Required | Notes |
32
36
  |---|---|:-:|---|
33
- | `shopDomain` | `string` | ✓ | Your shop domain. |
34
- | `storefrontAccessToken` | `string` | ✓ | Public Storefront Access Token. |
37
+ | `shopDomain` | `string` | ✓ | Your `*.myshopify.com` domain. |
38
+ | `storefrontAccessToken` | `string` | ✓ | Generated in `/app/settings/headless`. |
35
39
  | `bundleGid` | `string` | ✓ | Bundle metaobject GID. |
36
- | `onAddToCart` | `(lines: CartLineInput[]) => Promise<void>` | ✓ | Must return a Promise. Components `await` it for loading states. |
37
- | `appUrl` | `string` | | Required only if `analyticsEnabled !== false`. |
38
- | `analyticsEnabled` | `boolean` | | Default `true`. Set `false` to disable impression / add-to-cart POSTs. |
39
- | `onError` | `(error: Error) => void` | | Fires on `StorefrontApiError`, `BundleParseError`, or errors thrown from `onAddToCart`. |
40
- | `locale` | `string` | | BCP-47 locale tag. |
40
+ | `onAddToCart` | `(lines: CartLineInput[]) => Promise<void>` | ✓ | Must return a Promise; components await it for loading states. |
41
+ | `appUrl` | `string` | | Required only if analytics are enabled. |
42
+ | `analyticsEnabled` | `boolean` | | Default `true`. `false` suppresses impression + add-to-cart POSTs. |
43
+ | `onError` | `(error: Error) => void` | | Fires on fetch, parse, or cart failure. |
44
+ | `locale` | `string` | | BCP-47 tag forwarded to the Storefront API. |
41
45
  | `className` | `string` | | Applied to the outer wrapper. |
42
46
 
43
- The `CartLineInput` shape matches Shopify Hydrogen's. A structural-compat type test guards this across versions.
47
+ `CartLineInput` matches Shopify Hydrogen's shape. A structural-compat test guards the contract across versions. Always preserve `attributes[]` so `_lime_bundle_gid` reaches Shopify; the `orders/create` webhook keys off it for purchase attribution.
44
48
 
45
49
  ## Hooks
46
50
 
47
- ### `useBundleData`
51
+ ### `useBundleData({ shopDomain, storefrontAccessToken, bundleGid })`
48
52
 
49
- Client-side hook. Returns a discriminated union locked at v1.0.0:
53
+ Fetches a single bundle by GID. Discriminated-union result.
50
54
 
51
55
  ```tsx
52
56
  import { useBundleData } from "@lime-bundles/react";
53
57
 
54
58
  function MyBundle({ bundleGid }: { bundleGid: string }) {
55
- const result = useBundleData({
56
- shopDomain: "my-shop.myshopify.com",
57
- storefrontAccessToken: TOKEN,
58
- bundleGid,
59
- });
60
-
59
+ const result = useBundleData({ shopDomain, storefrontAccessToken, bundleGid });
61
60
  if (result.status === "loading") return <Skeleton />;
62
61
  if (result.status === "error") return <Error message={result.error.message} />;
63
-
64
- // result.status === "success": result.bundle is a ParsedBundle
65
62
  return <pre>{JSON.stringify(result.bundle, null, 2)}</pre>;
66
63
  }
67
64
  ```
68
65
 
69
- Fetches custom CSS in parallel and injects a scoped `<style>` tag on success. No action needed from you.
66
+ ### `useBundlesForProduct({ shopDomain, storefrontAccessToken, productHandle })`
67
+
68
+ Fetches every active bundle configured against a product. Equivalent to the web component's auto-detect mode. Inactive and out-of-schedule bundles are filtered out.
69
+
70
+ ```tsx
71
+ import { useBundlesForProduct } from "@lime-bundles/react";
72
+
73
+ const result = useBundlesForProduct({ shopDomain, storefrontAccessToken, productHandle });
74
+ if (result.status === "success") {
75
+ return result.bundles.map((b) => <FixedBundle key={b.id} bundleGid={b.id} {...cartProps} />);
76
+ }
77
+ ```
70
78
 
71
- Unmounting or changing the bundle GID cancels the in-flight request via `AbortController`.
79
+ Both hooks auto-fetch merchant custom CSS and inject it as a scoped `<style>` tag. Unmount / dependency change cancels the in-flight request.
72
80
 
73
- ## Async fetcher for RSC / SSR
81
+ ## Async fetchers for SSR / RSC
74
82
 
75
- Use `fetchBundleData` in a server component, Next.js `getServerSideProps`, or any non-React async context:
83
+ For Hydrogen loaders, Next.js Server Components, or `getServerSideProps`: skip the hook and use the async helpers directly.
76
84
 
77
85
  ```ts
78
- import { fetchBundleData } from "@lime-bundles/react";
86
+ import { fetchBundleData, fetchBundlesForProduct } from "@lime-bundles/react";
79
87
 
80
88
  const bundle = await fetchBundleData({
81
- shopDomain: "my-shop.myshopify.com",
82
- storefrontAccessToken: process.env.LIME_BUNDLES_TOKEN!,
83
- bundleGid: "gid://shopify/Metaobject/42",
84
- buyerIp: request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? undefined,
89
+ shopDomain,
90
+ storefrontAccessToken,
91
+ bundleGid,
92
+ buyerIp: request.headers.get("x-forwarded-for")?.split(",")[0].trim(),
85
93
  });
86
94
  ```
87
95
 
88
- `buyerIp` is required on SSR. Shopify may return `430 Security Rejection` for server-originated traffic on a private token without it. Pull the real client IP from whichever header your host sets (`x-forwarded-for` on Oxygen/Vercel/most hosts, `cf-connecting-ip` on Cloudflare, etc).
89
-
90
- Throws:
91
- - `StorefrontApiError`: network, HTTP, or GraphQL errors.
92
- - `BundleParseError`: metaobject missing, invalid type, inactive, not started, or expired. `error.reason` is a typed literal.
93
-
94
- ## The cart contract
96
+ `buyerIp` is mandatory on SSR. Shopify returns 430 Security Rejection for server-originated traffic without it.
95
97
 
96
- `onAddToCart` receives Hydrogen-compatible cart lines:
98
+ ## Build your own UI
97
99
 
98
- ```ts
99
- type CartLineInput = {
100
- merchandiseId: string; // variant GID
101
- quantity: number;
102
- attributes: Array<{ key: string; value: string }>;
103
- };
104
- ```
105
-
106
- Always preserve `attributes[].{key: "_lime_bundle_gid"}` on its way to Shopify. The `orders/create` webhook uses it for purchase attribution.
100
+ Everything the built-in components use is re-exported for DIY consumers: pricing math, CSS-variable flatteners, image CDN transforms, countdown formatter, A/B merge, etc. See the [Build your own UI](./docs/react-nextjs.md#build-your-own-ui-diy-path) doc for the full primitives list.
107
101
 
108
102
  ## Styling
109
103
 
110
- Override CSS custom properties on any wrapping element:
104
+ Merchant widget styling is applied automatically when you use `<FixedBundle>` / `<VolumeBundle>` / `<MixMatchBundle>`. For DIY UI, apply the config yourself:
111
105
 
112
106
  ```tsx
113
- <div style={{ "--lb-primary-color": "#e91e63" } as React.CSSProperties}>
114
- <FixedBundle ... />
115
- </div>
107
+ import { applyWidgetConfigVars } from "@lime-bundles/react";
108
+
109
+ useEffect(() => {
110
+ if (ref.current) applyWidgetConfigVars(ref.current, bundle.widgetConfig);
111
+ }, [bundle.widgetConfig]);
116
112
  ```
117
113
 
118
- Full reference: [css-variables.md](https://github.com/lime-app-dev/lime-bundles-app/blob/main/docs/headless/css-variables.md).
114
+ Full CSS variable reference: [css-variables.md](./docs/css-variables.md).
119
115
 
120
- ## Versioning
116
+ ## Version policy
121
117
 
122
- Major versions bump together across `core`, `react`, and `widget`. The hook's `UseBundleDataResult` type and `BundleComponentProps` are locked at v1.0.0.
118
+ `BundleComponentProps`, `UseBundleDataResult`, and `UseBundlesForProductResult` are locked at v2.0.0. All three packages (`core`, `react`, `widget`) bump majors together.
123
119
 
124
120
  ## License
125
121
 
126
- MIT. See repo root.
122
+ MIT.
127
123
 
128
- ## Links
124
+ ## Support
129
125
 
130
- - [Hydrogen guide](https://github.com/lime-app-dev/lime-bundles-app/blob/main/docs/headless/hydrogen.md)
131
- - [React / Next.js guide](https://github.com/lime-app-dev/lime-bundles-app/blob/main/docs/headless/react-nextjs.md)
132
- - [Report issues](https://github.com/lime-app-dev/lime-bundles-app/issues)
126
+ Merchant support and bug reports: email via the Lime Bundles listing on the Shopify App Store.
package/dist/index.cjs CHANGED
@@ -20,15 +20,47 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- BundleParseError: () => import_core8.BundleParseError,
23
+ BUNDLES_FOR_PRODUCT_QUERY: () => import_core10.BUNDLES_FOR_PRODUCT_QUERY,
24
+ BUNDLE_METAOBJECT_QUERY: () => import_core10.BUNDLE_METAOBJECT_QUERY,
25
+ BundleParseError: () => import_core9.BundleParseError,
24
26
  FixedBundle: () => FixedBundle,
25
27
  MixMatchBundle: () => MixMatchBundle,
26
- StorefrontApiError: () => import_core8.StorefrontApiError,
28
+ SHOP_CUSTOM_CSS_QUERY: () => import_core10.SHOP_CUSTOM_CSS_QUERY,
29
+ StorefrontApiError: () => import_core9.StorefrontApiError,
27
30
  VolumeBundle: () => VolumeBundle,
31
+ WIDGET_CONFIG_DEFAULTS: () => import_core10.WIDGET_CONFIG_DEFAULTS,
32
+ applyABVariantB: () => import_core10.applyABVariantB,
33
+ applyWidgetConfigVars: () => import_core10.applyWidgetConfigVars,
34
+ calculateDiscount: () => import_core10.calculateDiscount,
35
+ calculateTierSavings: () => import_core10.calculateTierSavings,
36
+ computeBundleSaleCents: () => import_core10.computeBundleSaleCents,
37
+ computeFixedPricing: () => import_core10.computeFixedPricing,
38
+ createStorefrontClient: () => import_core10.createStorefrontClient,
28
39
  fetchBundleData: () => fetchBundleData,
40
+ fetchBundlesForProduct: () => import_core10.fetchBundlesForProduct,
29
41
  fetchShopCustomCss: () => fetchShopCustomCss,
42
+ formatCents: () => import_core10.formatCents,
43
+ formatCountdown: () => import_core10.formatCountdown,
44
+ formatMoney: () => import_core10.formatMoney,
45
+ getABTestAssignment: () => import_core10.getABTestAssignment,
46
+ getActiveTier: () => import_core10.getActiveTier,
47
+ hasConsent: () => import_core10.hasConsent,
48
+ injectCustomCss: () => import_core10.injectCustomCss,
49
+ mergeWidgetConfig: () => import_core10.mergeWidgetConfig,
50
+ observeImpression: () => import_core10.observeImpression,
51
+ parseCents: () => import_core10.parseCents,
52
+ parseMetaobjectBundle: () => import_core10.parseMetaobjectBundle,
53
+ parseMetaobjectBundleStrict: () => import_core10.parseMetaobjectBundleStrict,
54
+ percentageDiscountUnit: () => import_core10.percentageDiscountUnit,
55
+ reportAddToCart: () => import_core10.reportAddToCart,
56
+ reportImpression: () => import_core10.reportImpression,
57
+ sanitizeCustomCss: () => import_core10.sanitizeCustomCss,
58
+ setConsent: () => import_core10.setConsent,
59
+ transformImageUrl: () => import_core10.transformImageUrl,
30
60
  useAnalytics: () => useAnalytics,
31
- useBundleData: () => useBundleData
61
+ useBundleData: () => useBundleData,
62
+ useBundlesForProduct: () => useBundlesForProduct,
63
+ validateQuantity: () => import_core10.validateQuantity
32
64
  });
33
65
  module.exports = __toCommonJS(index_exports);
34
66
 
@@ -328,7 +360,7 @@ function FixedBundle(props) {
328
360
  onClick: handleAddToCart,
329
361
  disabled: addingToCart,
330
362
  "aria-busy": addingToCart,
331
- children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? "Add Bundle to Cart"
363
+ children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? "Add Bundle to Cart"
332
364
  }
333
365
  )
334
366
  ]
@@ -554,7 +586,7 @@ function MixMatchBundle(props) {
554
586
  onClick: handleAddToCart,
555
587
  disabled: addingToCart || !validation.valid,
556
588
  "aria-busy": addingToCart,
557
- children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${totalQuantity} Items to Cart`
589
+ children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${totalQuantity} Items to Cart`
558
590
  }
559
591
  )
560
592
  ]
@@ -609,7 +641,12 @@ function VolumeBundle(props) {
609
641
  const product = bundle?.products[0];
610
642
  const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
611
643
  const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
612
- const tierSavings = bundle ? (0, import_core7.calculateTierSavings)(bundle.volumeTiers, basePrice, quantity) : [];
644
+ const tierSavings = bundle ? (0, import_core7.calculateTierSavings)(
645
+ bundle.volumeTiers,
646
+ basePrice,
647
+ quantity,
648
+ bundle.discountConfig.discountType
649
+ ) : [];
613
650
  const activeTier = bundle ? (0, import_core7.getActiveTier)(bundle.volumeTiers, quantity) : null;
614
651
  const handleAddToCart = (0, import_react5.useCallback)(async () => {
615
652
  if (!bundle || !product) return;
@@ -629,7 +666,7 @@ function VolumeBundle(props) {
629
666
  setCartError(null);
630
667
  try {
631
668
  await onAddToCart(lines);
632
- const unitPrice = activeTier ? basePrice * (1 - activeTier.discountValue / 100) : basePrice;
669
+ const unitPrice = tierSavings.find((ts) => ts.tier === activeTier)?.unitPrice ?? basePrice;
633
670
  trackAddToCart({
634
671
  productId: product.id,
635
672
  quantity,
@@ -712,8 +749,7 @@ function VolumeBundle(props) {
712
749
  "Save ",
713
750
  ts.savingsPercent.toFixed(0),
714
751
  "%"
715
- ] }),
716
- ts.tier.label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "lb-bundle__tier-label", role: "cell", children: ts.tier.label })
752
+ ] })
717
753
  ]
718
754
  },
719
755
  ts.tier.minQuantity
@@ -763,7 +799,7 @@ function VolumeBundle(props) {
763
799
  onClick: handleAddToCart,
764
800
  disabled: addingToCart,
765
801
  "aria-busy": addingToCart,
766
- children: addingToCart ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`
802
+ children: addingToCart ? "Adding..." : bundle.widgetConfig.cta.ctaText ?? `Add ${quantity} to Cart`
767
803
  }
768
804
  )
769
805
  ]
@@ -771,18 +807,104 @@ function VolumeBundle(props) {
771
807
  );
772
808
  }
773
809
 
774
- // src/index.ts
810
+ // src/hooks/useBundlesForProduct.ts
811
+ var import_react6 = require("react");
775
812
  var import_core8 = require("@lime-bundles/core");
813
+ var INITIAL_STATE2 = {
814
+ status: "loading",
815
+ bundles: null,
816
+ error: null
817
+ };
818
+ function useBundlesForProduct(options) {
819
+ const [state, setState] = (0, import_react6.useState)(
820
+ INITIAL_STATE2
821
+ );
822
+ (0, import_react6.useEffect)(() => {
823
+ const controller = new AbortController();
824
+ setState(INITIAL_STATE2);
825
+ (0, import_core8.fetchBundlesForProduct)({
826
+ shopDomain: options.shopDomain,
827
+ storefrontAccessToken: options.storefrontAccessToken,
828
+ productHandle: options.productHandle,
829
+ buyerIp: options.buyerIp,
830
+ signal: controller.signal
831
+ }).then((bundles) => {
832
+ if (controller.signal.aborted) return;
833
+ setState({ status: "success", bundles, error: null });
834
+ }).catch((err) => {
835
+ if (controller.signal.aborted) return;
836
+ setState({
837
+ status: "error",
838
+ bundles: null,
839
+ error: err instanceof Error ? err : new Error(String(err))
840
+ });
841
+ });
842
+ fetchShopCustomCss({
843
+ shopDomain: options.shopDomain,
844
+ storefrontAccessToken: options.storefrontAccessToken,
845
+ signal: controller.signal
846
+ }).then((css) => {
847
+ if (controller.signal.aborted) return;
848
+ (0, import_core8.injectCustomCss)(options.shopDomain, css);
849
+ }).catch(() => {
850
+ });
851
+ return () => {
852
+ controller.abort();
853
+ };
854
+ }, [
855
+ options.shopDomain,
856
+ options.storefrontAccessToken,
857
+ options.productHandle,
858
+ options.buyerIp
859
+ ]);
860
+ return state;
861
+ }
862
+
863
+ // src/index.ts
864
+ var import_core9 = require("@lime-bundles/core");
865
+ var import_core10 = require("@lime-bundles/core");
776
866
  // Annotate the CommonJS export names for ESM import in node:
777
867
  0 && (module.exports = {
868
+ BUNDLES_FOR_PRODUCT_QUERY,
869
+ BUNDLE_METAOBJECT_QUERY,
778
870
  BundleParseError,
779
871
  FixedBundle,
780
872
  MixMatchBundle,
873
+ SHOP_CUSTOM_CSS_QUERY,
781
874
  StorefrontApiError,
782
875
  VolumeBundle,
876
+ WIDGET_CONFIG_DEFAULTS,
877
+ applyABVariantB,
878
+ applyWidgetConfigVars,
879
+ calculateDiscount,
880
+ calculateTierSavings,
881
+ computeBundleSaleCents,
882
+ computeFixedPricing,
883
+ createStorefrontClient,
783
884
  fetchBundleData,
885
+ fetchBundlesForProduct,
784
886
  fetchShopCustomCss,
887
+ formatCents,
888
+ formatCountdown,
889
+ formatMoney,
890
+ getABTestAssignment,
891
+ getActiveTier,
892
+ hasConsent,
893
+ injectCustomCss,
894
+ mergeWidgetConfig,
895
+ observeImpression,
896
+ parseCents,
897
+ parseMetaobjectBundle,
898
+ parseMetaobjectBundleStrict,
899
+ percentageDiscountUnit,
900
+ reportAddToCart,
901
+ reportImpression,
902
+ sanitizeCustomCss,
903
+ setConsent,
904
+ transformImageUrl,
785
905
  useAnalytics,
786
- useBundleData
906
+ useBundleData,
907
+ useBundlesForProduct,
908
+ validateQuantity
787
909
  });
788
910
  //# sourceMappingURL=index.cjs.map