@cimplify/sdk 0.24.1 → 0.24.2

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/react.d.mts CHANGED
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
  import { A as AdSlot, a as AdPosition, e as AdContextValue } from './ads-BxbWrwqp.mjs';
3
3
  export { c as AdConfig, g as getVariantDisplayName } from './ads-BxbWrwqp.mjs';
4
4
  import { C as CimplifyClient, a8 as ProcessCheckoutResult, aa as CheckoutStatus, ab as CheckoutStatusContext, bK as Location, bH as Business, k as ReorderResult, aR as OrderStatus, b2 as Order, d as QuoteBundleSelectionInput, Q as QuoteCompositeSelectionInput, P as PriceQuote, g as QuoteUiMessage, bX as Service, c0 as AvailableSlot, c1 as DayAvailability, c6 as CustomerBooking, cc as CancelBookingResult, w as ActivityStateResponse, u as SessionMessage, x as ActivityRecommendation, E as DeliveryFeeResponse, cO as FxRateResponse, bR as StorefrontBootstrap, Y as ElementsOptions, d0 as AuthenticatedData, cY as AddressInfo, cZ as PaymentMethodInfo, K as CimplifyElements, d2 as ElementsCheckoutResult, a7 as ProcessCheckoutOptions, b1 as LineItem, bY as Staff, bP as Room } from './index-DIHoZUz_.mjs';
5
- import { J as Product, d as Pagination, h as CimplifyError, L as ProductWithDetails, a6 as Category, M as Money, b1 as BundleSelectionInput, aw as ComponentSelectionInput, b4 as BundleSelectionData, b7 as CompositeSelectionData, a8 as Collection, aE as ProductTaxonomy, aF as TaxonomyWithChildren, aD as ProductTimeProfile, aI as Deal, aJ as ProductDealInfo, aK as DiscountValidation, ad as BundleSummary, ax as CompositePriceResult, y as CustomAttributeDefinition, G as PropertyFacet, bJ as ProductBillingPlan, N as VariantView, X as VariantAxisWithValues, a as CurrencyCode, a2 as AddOnWithOptions, ai as BundleComponentView, ab as BundlePriceType, at as CompositeGroupView, bc as DisplayCart, z as CustomAttributeValue, F as ProductProperty, Q as QuantityPricingTier } from './payment-DqiwuLma.mjs';
5
+ import { a as CurrencyCode, J as Product, d as Pagination, h as CimplifyError, L as ProductWithDetails, a6 as Category, M as Money, b1 as BundleSelectionInput, aw as ComponentSelectionInput, b4 as BundleSelectionData, b7 as CompositeSelectionData, a8 as Collection, aE as ProductTaxonomy, aF as TaxonomyWithChildren, aD as ProductTimeProfile, aI as Deal, aJ as ProductDealInfo, aK as DiscountValidation, ad as BundleSummary, ax as CompositePriceResult, y as CustomAttributeDefinition, G as PropertyFacet, bJ as ProductBillingPlan, N as VariantView, X as VariantAxisWithValues, a2 as AddOnWithOptions, ai as BundleComponentView, ab as BundlePriceType, at as CompositeGroupView, bc as DisplayCart, z as CustomAttributeValue, F as ProductProperty, Q as QuantityPricingTier } from './payment-DqiwuLma.mjs';
6
6
  import { P as ProductWithPrice } from './price-DVQrpE7j.mjs';
7
7
  import { ClassValue } from 'clsx';
8
8
 
@@ -84,18 +84,23 @@ declare function CimplifyCheckout({ client, businessId, cartId, locationId, link
84
84
  interface PriceProps {
85
85
  /** The amount in base (business) currency. */
86
86
  amount: number | string;
87
+ /** Explicit currency — skips provider lookup when provided. */
88
+ currency?: CurrencyCode;
87
89
  /** Optional CSS class name for the wrapping span. */
88
90
  className?: string;
89
91
  /** Optional prefix rendered before the formatted price (e.g. "+"). */
90
92
  prefix?: string;
91
93
  }
92
94
  /**
93
- * Price — renders a single price value in the user's chosen display currency.
95
+ * Price — renders a formatted price value.
96
+ *
97
+ * When used inside CimplifyProvider: reads displayCurrency and convertPrice
98
+ * from context for FX conversion.
94
99
  *
95
- * Reads `displayCurrency` and `convertPrice` from CimplifyProvider context
96
- * so every price on the page stays consistent with the selected currency.
100
+ * When used outside a provider (or with explicit `currency` prop): formats
101
+ * directly — no provider required. Works in any React environment.
97
102
  */
98
- declare function Price({ amount, className, prefix }: PriceProps): React.ReactElement;
103
+ declare function Price({ amount, currency, className, prefix }: PriceProps): React.ReactElement;
99
104
 
100
105
  interface CimplifyClientContextValue {
101
106
  client: CimplifyClient;
@@ -2337,10 +2342,11 @@ interface VolumePricingProps {
2337
2342
  declare function VolumePricing({ tiers, currentQuantity, defaultOpen, className, classNames, }: VolumePricingProps): React.ReactElement | null;
2338
2343
 
2339
2344
  interface PriceRangeProps {
2340
- product: Product;
2345
+ product: Product | ProductWithDetails;
2346
+ currency?: CurrencyCode;
2341
2347
  className?: string;
2342
2348
  }
2343
- declare function PriceRange({ product, className }: PriceRangeProps): React.ReactElement | null;
2349
+ declare function PriceRange({ product, currency, className }: PriceRangeProps): React.ReactElement | null;
2344
2350
 
2345
2351
  interface DeliveryEstimateClassNames {
2346
2352
  root?: string;
package/dist/react.d.ts CHANGED
@@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
2
2
  import { A as AdSlot, a as AdPosition, e as AdContextValue } from './ads-BxbWrwqp.js';
3
3
  export { c as AdConfig, g as getVariantDisplayName } from './ads-BxbWrwqp.js';
4
4
  import { C as CimplifyClient, a8 as ProcessCheckoutResult, aa as CheckoutStatus, ab as CheckoutStatusContext, bK as Location, bH as Business, k as ReorderResult, aR as OrderStatus, b2 as Order, d as QuoteBundleSelectionInput, Q as QuoteCompositeSelectionInput, P as PriceQuote, g as QuoteUiMessage, bX as Service, c0 as AvailableSlot, c1 as DayAvailability, c6 as CustomerBooking, cc as CancelBookingResult, w as ActivityStateResponse, u as SessionMessage, x as ActivityRecommendation, E as DeliveryFeeResponse, cO as FxRateResponse, bR as StorefrontBootstrap, Y as ElementsOptions, d0 as AuthenticatedData, cY as AddressInfo, cZ as PaymentMethodInfo, K as CimplifyElements, d2 as ElementsCheckoutResult, a7 as ProcessCheckoutOptions, b1 as LineItem, bY as Staff, bP as Room } from './index-CJZMsbDi.js';
5
- import { J as Product, d as Pagination, h as CimplifyError, L as ProductWithDetails, a6 as Category, M as Money, b1 as BundleSelectionInput, aw as ComponentSelectionInput, b4 as BundleSelectionData, b7 as CompositeSelectionData, a8 as Collection, aE as ProductTaxonomy, aF as TaxonomyWithChildren, aD as ProductTimeProfile, aI as Deal, aJ as ProductDealInfo, aK as DiscountValidation, ad as BundleSummary, ax as CompositePriceResult, y as CustomAttributeDefinition, G as PropertyFacet, bJ as ProductBillingPlan, N as VariantView, X as VariantAxisWithValues, a as CurrencyCode, a2 as AddOnWithOptions, ai as BundleComponentView, ab as BundlePriceType, at as CompositeGroupView, bc as DisplayCart, z as CustomAttributeValue, F as ProductProperty, Q as QuantityPricingTier } from './payment-DqiwuLma.js';
5
+ import { a as CurrencyCode, J as Product, d as Pagination, h as CimplifyError, L as ProductWithDetails, a6 as Category, M as Money, b1 as BundleSelectionInput, aw as ComponentSelectionInput, b4 as BundleSelectionData, b7 as CompositeSelectionData, a8 as Collection, aE as ProductTaxonomy, aF as TaxonomyWithChildren, aD as ProductTimeProfile, aI as Deal, aJ as ProductDealInfo, aK as DiscountValidation, ad as BundleSummary, ax as CompositePriceResult, y as CustomAttributeDefinition, G as PropertyFacet, bJ as ProductBillingPlan, N as VariantView, X as VariantAxisWithValues, a2 as AddOnWithOptions, ai as BundleComponentView, ab as BundlePriceType, at as CompositeGroupView, bc as DisplayCart, z as CustomAttributeValue, F as ProductProperty, Q as QuantityPricingTier } from './payment-DqiwuLma.js';
6
6
  import { P as ProductWithPrice } from './price-BZlddg1A.js';
7
7
  import { ClassValue } from 'clsx';
8
8
 
@@ -84,18 +84,23 @@ declare function CimplifyCheckout({ client, businessId, cartId, locationId, link
84
84
  interface PriceProps {
85
85
  /** The amount in base (business) currency. */
86
86
  amount: number | string;
87
+ /** Explicit currency — skips provider lookup when provided. */
88
+ currency?: CurrencyCode;
87
89
  /** Optional CSS class name for the wrapping span. */
88
90
  className?: string;
89
91
  /** Optional prefix rendered before the formatted price (e.g. "+"). */
90
92
  prefix?: string;
91
93
  }
92
94
  /**
93
- * Price — renders a single price value in the user's chosen display currency.
95
+ * Price — renders a formatted price value.
96
+ *
97
+ * When used inside CimplifyProvider: reads displayCurrency and convertPrice
98
+ * from context for FX conversion.
94
99
  *
95
- * Reads `displayCurrency` and `convertPrice` from CimplifyProvider context
96
- * so every price on the page stays consistent with the selected currency.
100
+ * When used outside a provider (or with explicit `currency` prop): formats
101
+ * directly — no provider required. Works in any React environment.
97
102
  */
98
- declare function Price({ amount, className, prefix }: PriceProps): React.ReactElement;
103
+ declare function Price({ amount, currency, className, prefix }: PriceProps): React.ReactElement;
99
104
 
100
105
  interface CimplifyClientContextValue {
101
106
  client: CimplifyClient;
@@ -2337,10 +2342,11 @@ interface VolumePricingProps {
2337
2342
  declare function VolumePricing({ tiers, currentQuantity, defaultOpen, className, classNames, }: VolumePricingProps): React.ReactElement | null;
2338
2343
 
2339
2344
  interface PriceRangeProps {
2340
- product: Product;
2345
+ product: Product | ProductWithDetails;
2346
+ currency?: CurrencyCode;
2341
2347
  className?: string;
2342
2348
  }
2343
- declare function PriceRange({ product, className }: PriceRangeProps): React.ReactElement | null;
2349
+ declare function PriceRange({ product, currency, className }: PriceRangeProps): React.ReactElement | null;
2344
2350
 
2345
2351
  interface DeliveryEstimateClassNames {
2346
2352
  root?: string;
package/dist/react.js CHANGED
@@ -1036,7 +1036,7 @@ function useCart(options = {}) {
1036
1036
  () => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
1037
1037
  [snapshot.items]
1038
1038
  );
1039
- return {
1039
+ return React3.useMemo(() => ({
1040
1040
  items: snapshot.items,
1041
1041
  itemCount,
1042
1042
  subtotal: snapshot.subtotal,
@@ -1051,7 +1051,7 @@ function useCart(options = {}) {
1051
1051
  clearCart,
1052
1052
  sync,
1053
1053
  reorder
1054
- };
1054
+ }), [snapshot.items, itemCount, snapshot.subtotal, snapshot.tax, snapshot.total, snapshot.currency, snapshot.isLoading, addItem, removeItem, updateQuantity, clearCart, sync, reorder]);
1055
1055
  }
1056
1056
  var LOCATION_STORAGE_KEY = "cimplify_location_id";
1057
1057
  var DISPLAY_CURRENCY_STORAGE_KEY = "cimplify_display_currency";
@@ -1702,11 +1702,14 @@ function CimplifyCheckout({
1702
1702
  errorMessage && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-cimplify-error": "", style: { marginTop: SPACE.sm, fontSize: 13, color: colors.error }, children: errorMessage })
1703
1703
  ] });
1704
1704
  }
1705
- function Price({ amount, className, prefix }) {
1706
- const { displayCurrency, convertPrice } = useCimplify();
1705
+ function Price({ amount, currency, className, prefix }) {
1706
+ const context = useOptionalCimplify();
1707
+ const resolvedCurrency = currency ?? context?.displayCurrency ?? "USD";
1708
+ const convertedAmount = !currency && context?.convertPrice ? context.convertPrice(amount) : amount;
1709
+ const resolvedAmount = typeof convertedAmount === "string" ? parseFloat(convertedAmount) || 0 : convertedAmount;
1707
1710
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { className, children: [
1708
1711
  prefix,
1709
- chunkPEDLCL72_js.formatPrice(convertPrice(amount), displayCurrency)
1712
+ chunkPEDLCL72_js.formatPrice(resolvedAmount, resolvedCurrency)
1710
1713
  ] });
1711
1714
  }
1712
1715
  function createCachedQuery(config) {
@@ -4806,7 +4809,9 @@ function ProductCustomizer({
4806
4809
  },
4807
4810
  []
4808
4811
  );
4809
- const handleAddToCart = async () => {
4812
+ const addedTimerRef = React3.useRef(void 0);
4813
+ React3.useEffect(() => () => clearTimeout(addedTimerRef.current), []);
4814
+ const handleAddToCart = React3.useCallback(async () => {
4810
4815
  if (isSubmitting) return;
4811
4816
  setIsSubmitting(true);
4812
4817
  const options = {
@@ -4832,15 +4837,16 @@ function ProductCustomizer({
4832
4837
  await cart.addItem(product, quantity, options);
4833
4838
  }
4834
4839
  setIsAdded(true);
4835
- setTimeout(() => {
4840
+ clearTimeout(addedTimerRef.current);
4841
+ addedTimerRef.current = setTimeout(() => {
4836
4842
  setIsAdded(false);
4837
- setQuantity(1);
4843
+ setQuantity(product.min_order_quantity ?? 1);
4838
4844
  }, 2e3);
4839
4845
  } catch {
4840
4846
  } finally {
4841
4847
  setIsSubmitting(false);
4842
4848
  }
4843
- };
4849
+ }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, specialInstructions, isSubmitting, onAddToCart, cart]);
4844
4850
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
4845
4851
  isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsxRuntime.jsx(
4846
4852
  CompositeSelector,
@@ -7626,11 +7632,12 @@ function getMetadataStringList(metadata, key) {
7626
7632
  if (!Array.isArray(value)) return [];
7627
7633
  return value.filter((item) => typeof item === "string");
7628
7634
  }
7629
- function PriceRange({ product, className }) {
7630
- const { displayCurrency } = useCimplify();
7635
+ function PriceRange({ product, currency, className }) {
7636
+ const context = useOptionalCimplify();
7637
+ const resolvedCurrency = currency ?? context?.displayCurrency ?? "USD";
7631
7638
  const range = chunkPEDLCL72_js.getPriceRange(product);
7632
7639
  if (!range) return null;
7633
- return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cimplify-price-range": true, className, children: chunkPEDLCL72_js.formatPriceRange(range.min, range.max, displayCurrency) });
7640
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cimplify-price-range": true, className, children: chunkPEDLCL72_js.formatPriceRange(range.min, range.max, resolvedCurrency) });
7634
7641
  }
7635
7642
  function DefaultProductLayout({
7636
7643
  product,
package/dist/react.mjs CHANGED
@@ -1031,7 +1031,7 @@ function useCart(options = {}) {
1031
1031
  () => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
1032
1032
  [snapshot.items]
1033
1033
  );
1034
- return {
1034
+ return useMemo(() => ({
1035
1035
  items: snapshot.items,
1036
1036
  itemCount,
1037
1037
  subtotal: snapshot.subtotal,
@@ -1046,7 +1046,7 @@ function useCart(options = {}) {
1046
1046
  clearCart,
1047
1047
  sync,
1048
1048
  reorder
1049
- };
1049
+ }), [snapshot.items, itemCount, snapshot.subtotal, snapshot.tax, snapshot.total, snapshot.currency, snapshot.isLoading, addItem, removeItem, updateQuantity, clearCart, sync, reorder]);
1050
1050
  }
1051
1051
  var LOCATION_STORAGE_KEY = "cimplify_location_id";
1052
1052
  var DISPLAY_CURRENCY_STORAGE_KEY = "cimplify_display_currency";
@@ -1697,11 +1697,14 @@ function CimplifyCheckout({
1697
1697
  errorMessage && /* @__PURE__ */ jsx("p", { "data-cimplify-error": "", style: { marginTop: SPACE.sm, fontSize: 13, color: colors.error }, children: errorMessage })
1698
1698
  ] });
1699
1699
  }
1700
- function Price({ amount, className, prefix }) {
1701
- const { displayCurrency, convertPrice } = useCimplify();
1700
+ function Price({ amount, currency, className, prefix }) {
1701
+ const context = useOptionalCimplify();
1702
+ const resolvedCurrency = currency ?? context?.displayCurrency ?? "USD";
1703
+ const convertedAmount = !currency && context?.convertPrice ? context.convertPrice(amount) : amount;
1704
+ const resolvedAmount = typeof convertedAmount === "string" ? parseFloat(convertedAmount) || 0 : convertedAmount;
1702
1705
  return /* @__PURE__ */ jsxs("span", { className, children: [
1703
1706
  prefix,
1704
- formatPrice(convertPrice(amount), displayCurrency)
1707
+ formatPrice(resolvedAmount, resolvedCurrency)
1705
1708
  ] });
1706
1709
  }
1707
1710
  function createCachedQuery(config) {
@@ -4801,7 +4804,9 @@ function ProductCustomizer({
4801
4804
  },
4802
4805
  []
4803
4806
  );
4804
- const handleAddToCart = async () => {
4807
+ const addedTimerRef = useRef(void 0);
4808
+ useEffect(() => () => clearTimeout(addedTimerRef.current), []);
4809
+ const handleAddToCart = useCallback(async () => {
4805
4810
  if (isSubmitting) return;
4806
4811
  setIsSubmitting(true);
4807
4812
  const options = {
@@ -4827,15 +4832,16 @@ function ProductCustomizer({
4827
4832
  await cart.addItem(product, quantity, options);
4828
4833
  }
4829
4834
  setIsAdded(true);
4830
- setTimeout(() => {
4835
+ clearTimeout(addedTimerRef.current);
4836
+ addedTimerRef.current = setTimeout(() => {
4831
4837
  setIsAdded(false);
4832
- setQuantity(1);
4838
+ setQuantity(product.min_order_quantity ?? 1);
4833
4839
  }, 2e3);
4834
4840
  } catch {
4835
4841
  } finally {
4836
4842
  setIsSubmitting(false);
4837
4843
  }
4838
- };
4844
+ }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, specialInstructions, isSubmitting, onAddToCart, cart]);
4839
4845
  return /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
4840
4846
  isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsx(
4841
4847
  CompositeSelector,
@@ -7621,11 +7627,12 @@ function getMetadataStringList(metadata, key) {
7621
7627
  if (!Array.isArray(value)) return [];
7622
7628
  return value.filter((item) => typeof item === "string");
7623
7629
  }
7624
- function PriceRange({ product, className }) {
7625
- const { displayCurrency } = useCimplify();
7630
+ function PriceRange({ product, currency, className }) {
7631
+ const context = useOptionalCimplify();
7632
+ const resolvedCurrency = currency ?? context?.displayCurrency ?? "USD";
7626
7633
  const range = getPriceRange(product);
7627
7634
  if (!range) return null;
7628
- return /* @__PURE__ */ jsx("span", { "data-cimplify-price-range": true, className, children: formatPriceRange(range.min, range.max, displayCurrency) });
7635
+ return /* @__PURE__ */ jsx("span", { "data-cimplify-price-range": true, className, children: formatPriceRange(range.min, range.max, resolvedCurrency) });
7629
7636
  }
7630
7637
  function DefaultProductLayout({
7631
7638
  product,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -7,7 +7,7 @@
7
7
  "files": [
8
8
  {
9
9
  "path": "price-range.tsx",
10
- "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { getPriceRange, formatPriceRange } from \"@cimplify/sdk\";\nimport { useCimplify } from \"@cimplify/sdk/react\";\n\nexport interface PriceRangeProps {\n product: Product;\n className?: string;\n}\n\nexport function PriceRange({ product, className }: PriceRangeProps): React.ReactElement | null {\n const { displayCurrency } = useCimplify();\n const range = getPriceRange(product);\n\n if (!range) return null;\n\n return (\n <span data-cimplify-price-range className={className}>\n {formatPriceRange(range.min, range.max, displayCurrency as CurrencyCode)}\n </span>\n );\n}\n"
10
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { getPriceRange, formatPriceRange } from \"@cimplify/sdk\";\nimport { useOptionalCimplify } from \"@cimplify/sdk/react\";\n\nexport interface PriceRangeProps {\n product: Product | ProductWithDetails;\n currency?: CurrencyCode;\n className?: string;\n}\n\nexport function PriceRange({ product, currency, className }: PriceRangeProps): React.ReactElement | null {\n const context = useOptionalCimplify();\n const resolvedCurrency = currency ?? (context?.displayCurrency as CurrencyCode | undefined) ?? \"USD\";\n const range = getPriceRange(product);\n\n if (!range) return null;\n\n return (\n <span data-cimplify-price-range className={className}>\n {formatPriceRange(range.min, range.max, resolvedCurrency)}\n </span>\n );\n}\n"
11
11
  }
12
12
  ]
13
13
  }
@@ -7,7 +7,7 @@
7
7
  "files": [
8
8
  {
9
9
  "path": "price.tsx",
10
- "content": "\"use client\";\n\nimport React from \"react\";\nimport { formatPrice } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useCimplify } from \"@cimplify/sdk/react\";\n\nexport interface PriceProps {\n /** The amount in base (business) currency. */\n amount: number | string;\n /** Optional CSS class name for the wrapping span. */\n className?: string;\n /** Optional prefix rendered before the formatted price (e.g. \"+\"). */\n prefix?: string;\n}\n\n/**\n * Price — renders a single price value in the user's chosen display currency.\n *\n * Reads `displayCurrency` and `convertPrice` from CimplifyProvider context\n * so every price on the page stays consistent with the selected currency.\n */\nexport function Price({ amount, className, prefix }: PriceProps): React.ReactElement {\n const { displayCurrency, convertPrice } = useCimplify();\n return (\n <span className={className}>\n {prefix}\n {formatPrice(convertPrice(amount), displayCurrency as CurrencyCode)}\n </span>\n );\n}\n"
10
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { formatPrice } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useOptionalCimplify } from \"@cimplify/sdk/react\";\n\nexport interface PriceProps {\n /** The amount in base (business) currency. */\n amount: number | string;\n /** Explicit currency — skips provider lookup when provided. */\n currency?: CurrencyCode;\n /** Optional CSS class name for the wrapping span. */\n className?: string;\n /** Optional prefix rendered before the formatted price (e.g. \"+\"). */\n prefix?: string;\n}\n\n/**\n * Price — renders a formatted price value.\n *\n * When used inside CimplifyProvider: reads displayCurrency and convertPrice\n * from context for FX conversion.\n *\n * When used outside a provider (or with explicit `currency` prop): formats\n * directly — no provider required. Works in any React environment.\n */\nexport function Price({ amount, currency, className, prefix }: PriceProps): React.ReactElement {\n const context = useOptionalCimplify();\n const resolvedCurrency = currency ?? (context?.displayCurrency as CurrencyCode | undefined) ?? \"USD\";\n const convertedAmount = !currency && context?.convertPrice ? context.convertPrice(amount) : amount;\n const resolvedAmount = typeof convertedAmount === \"string\" ? parseFloat(convertedAmount) || 0 : convertedAmount;\n\n return (\n <span className={className}>\n {prefix}\n {formatPrice(resolvedAmount, resolvedCurrency)}\n </span>\n );\n}\n"
11
11
  }
12
12
  ]
13
13
  }
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "product-customizer.tsx",
17
- "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setSpecialInstructions(\"\");\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const handleAddToCart = async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n setTimeout(() => {\n setIsAdded(false);\n setQuantity(1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {product.duration_minutes} {product.duration_unit ?? \"minutes\"}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
17
+ "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setSpecialInstructions(\"\");\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, specialInstructions, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {product.duration_minutes} {product.duration_unit ?? \"minutes\"}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
18
18
  }
19
19
  ]
20
20
  }