@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 +13 -7
- package/dist/react.d.ts +13 -7
- package/dist/react.js +19 -12
- package/dist/react.mjs +19 -12
- package/package.json +1 -1
- package/registry/price-range.json +1 -1
- package/registry/price.json +1 -1
- package/registry/product-customizer.json +1 -1
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,
|
|
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
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
@@ -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 {
|
|
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
|
}
|
package/registry/price.json
CHANGED
|
@@ -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 {
|
|
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 · <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 · <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|