@cimplify/sdk 0.22.5 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.d.mts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +21 -21
- package/dist/advanced.mjs +2 -2
- package/dist/{chunk-Y2BD2XFJ.mjs → chunk-3GSXQNWV.mjs} +2 -2
- package/dist/{chunk-DNN5VN64.mjs → chunk-BYQPMSH2.mjs} +1 -1
- package/dist/{chunk-5FK7MDHS.js → chunk-KRNWMUBN.js} +34 -0
- package/dist/{chunk-4Z74TIJP.mjs → chunk-OANVYH27.mjs} +32 -1
- package/dist/{chunk-KLRTWBDF.js → chunk-RAKLT6J2.js} +39 -39
- package/dist/{chunk-YMXFVI3N.js → chunk-TXQHTHY5.js} +20 -20
- package/dist/index-BodyCOun.d.mts +70 -0
- package/dist/{index-uvCOGdBp.d.ts → index-CJZMsbDi.d.ts} +1 -1
- package/dist/{index-v1NFqZFG.d.mts → index-DIHoZUz_.d.mts} +1 -1
- package/dist/index-IH1wOTmD.d.ts +70 -0
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +110 -98
- package/dist/index.mjs +3 -3
- package/dist/{payment-DazihbCY.d.mts → payment-DqiwuLma.d.mts} +9 -1
- package/dist/{payment-DazihbCY.d.ts → payment-DqiwuLma.d.ts} +9 -1
- package/dist/{price-5_L8WK6Q.d.ts → price-BZlddg1A.d.ts} +1 -1
- package/dist/{price-BcHVTqqu.d.mts → price-DVQrpE7j.d.mts} +1 -1
- package/dist/react.d.mts +30 -4
- package/dist/react.d.ts +30 -4
- package/dist/react.js +300 -169
- package/dist/react.mjs +240 -111
- package/dist/styles.css +1 -1
- package/dist/utils.d.mts +4 -64
- package/dist/utils.d.ts +4 -64
- package/dist/utils.js +28 -28
- package/dist/utils.mjs +1 -1
- package/package.json +1 -1
- package/registry/product-customizer.json +1 -1
package/dist/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunkL5GKVQVJ_js = require('./chunk-L5GKVQVJ.js');
|
|
4
|
-
var
|
|
4
|
+
var chunkKRNWMUBN_js = require('./chunk-KRNWMUBN.js');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
@@ -19,109 +19,109 @@ Object.defineProperty(exports, "resolvePhoneCountryCode", {
|
|
|
19
19
|
});
|
|
20
20
|
Object.defineProperty(exports, "CURRENCY_SYMBOLS", {
|
|
21
21
|
enumerable: true,
|
|
22
|
-
get: function () { return
|
|
22
|
+
get: function () { return chunkKRNWMUBN_js.CURRENCY_SYMBOLS; }
|
|
23
23
|
});
|
|
24
24
|
Object.defineProperty(exports, "MOBILE_MONEY_PROVIDERS", {
|
|
25
25
|
enumerable: true,
|
|
26
|
-
get: function () { return
|
|
26
|
+
get: function () { return chunkKRNWMUBN_js.MOBILE_MONEY_PROVIDERS; }
|
|
27
27
|
});
|
|
28
28
|
Object.defineProperty(exports, "categorizePaymentError", {
|
|
29
29
|
enumerable: true,
|
|
30
|
-
get: function () { return
|
|
30
|
+
get: function () { return chunkKRNWMUBN_js.categorizePaymentError; }
|
|
31
31
|
});
|
|
32
32
|
Object.defineProperty(exports, "detectMobileMoneyProvider", {
|
|
33
33
|
enumerable: true,
|
|
34
|
-
get: function () { return
|
|
34
|
+
get: function () { return chunkKRNWMUBN_js.detectMobileMoneyProvider; }
|
|
35
35
|
});
|
|
36
36
|
Object.defineProperty(exports, "formatMoney", {
|
|
37
37
|
enumerable: true,
|
|
38
|
-
get: function () { return
|
|
38
|
+
get: function () { return chunkKRNWMUBN_js.formatMoney; }
|
|
39
39
|
});
|
|
40
40
|
Object.defineProperty(exports, "formatNumberCompact", {
|
|
41
41
|
enumerable: true,
|
|
42
|
-
get: function () { return
|
|
42
|
+
get: function () { return chunkKRNWMUBN_js.formatNumberCompact; }
|
|
43
43
|
});
|
|
44
44
|
Object.defineProperty(exports, "formatPrice", {
|
|
45
45
|
enumerable: true,
|
|
46
|
-
get: function () { return
|
|
46
|
+
get: function () { return chunkKRNWMUBN_js.formatPrice; }
|
|
47
47
|
});
|
|
48
48
|
Object.defineProperty(exports, "formatPriceAdjustment", {
|
|
49
49
|
enumerable: true,
|
|
50
|
-
get: function () { return
|
|
50
|
+
get: function () { return chunkKRNWMUBN_js.formatPriceAdjustment; }
|
|
51
51
|
});
|
|
52
52
|
Object.defineProperty(exports, "formatPriceCompact", {
|
|
53
53
|
enumerable: true,
|
|
54
|
-
get: function () { return
|
|
54
|
+
get: function () { return chunkKRNWMUBN_js.formatPriceCompact; }
|
|
55
55
|
});
|
|
56
56
|
Object.defineProperty(exports, "formatPriceWithTax", {
|
|
57
57
|
enumerable: true,
|
|
58
|
-
get: function () { return
|
|
58
|
+
get: function () { return chunkKRNWMUBN_js.formatPriceWithTax; }
|
|
59
59
|
});
|
|
60
60
|
Object.defineProperty(exports, "formatProductPrice", {
|
|
61
61
|
enumerable: true,
|
|
62
|
-
get: function () { return
|
|
62
|
+
get: function () { return chunkKRNWMUBN_js.formatProductPrice; }
|
|
63
63
|
});
|
|
64
64
|
Object.defineProperty(exports, "getBasePrice", {
|
|
65
65
|
enumerable: true,
|
|
66
|
-
get: function () { return
|
|
66
|
+
get: function () { return chunkKRNWMUBN_js.getBasePrice; }
|
|
67
67
|
});
|
|
68
68
|
Object.defineProperty(exports, "getCurrencySymbol", {
|
|
69
69
|
enumerable: true,
|
|
70
|
-
get: function () { return
|
|
70
|
+
get: function () { return chunkKRNWMUBN_js.getCurrencySymbol; }
|
|
71
71
|
});
|
|
72
72
|
Object.defineProperty(exports, "getDiscountPercentage", {
|
|
73
73
|
enumerable: true,
|
|
74
|
-
get: function () { return
|
|
74
|
+
get: function () { return chunkKRNWMUBN_js.getDiscountPercentage; }
|
|
75
75
|
});
|
|
76
76
|
Object.defineProperty(exports, "getDisplayPrice", {
|
|
77
77
|
enumerable: true,
|
|
78
|
-
get: function () { return
|
|
78
|
+
get: function () { return chunkKRNWMUBN_js.getDisplayPrice; }
|
|
79
79
|
});
|
|
80
80
|
Object.defineProperty(exports, "getMarkupPercentage", {
|
|
81
81
|
enumerable: true,
|
|
82
|
-
get: function () { return
|
|
82
|
+
get: function () { return chunkKRNWMUBN_js.getMarkupPercentage; }
|
|
83
83
|
});
|
|
84
84
|
Object.defineProperty(exports, "getProductCurrency", {
|
|
85
85
|
enumerable: true,
|
|
86
|
-
get: function () { return
|
|
86
|
+
get: function () { return chunkKRNWMUBN_js.getProductCurrency; }
|
|
87
87
|
});
|
|
88
88
|
Object.defineProperty(exports, "getTaxAmount", {
|
|
89
89
|
enumerable: true,
|
|
90
|
-
get: function () { return
|
|
90
|
+
get: function () { return chunkKRNWMUBN_js.getTaxAmount; }
|
|
91
91
|
});
|
|
92
92
|
Object.defineProperty(exports, "hasTaxInfo", {
|
|
93
93
|
enumerable: true,
|
|
94
|
-
get: function () { return
|
|
94
|
+
get: function () { return chunkKRNWMUBN_js.hasTaxInfo; }
|
|
95
95
|
});
|
|
96
96
|
Object.defineProperty(exports, "isOnSale", {
|
|
97
97
|
enumerable: true,
|
|
98
|
-
get: function () { return
|
|
98
|
+
get: function () { return chunkKRNWMUBN_js.isOnSale; }
|
|
99
99
|
});
|
|
100
100
|
Object.defineProperty(exports, "isPaymentStatusFailure", {
|
|
101
101
|
enumerable: true,
|
|
102
|
-
get: function () { return
|
|
102
|
+
get: function () { return chunkKRNWMUBN_js.isPaymentStatusFailure; }
|
|
103
103
|
});
|
|
104
104
|
Object.defineProperty(exports, "isPaymentStatusRequiresAction", {
|
|
105
105
|
enumerable: true,
|
|
106
|
-
get: function () { return
|
|
106
|
+
get: function () { return chunkKRNWMUBN_js.isPaymentStatusRequiresAction; }
|
|
107
107
|
});
|
|
108
108
|
Object.defineProperty(exports, "isPaymentStatusSuccess", {
|
|
109
109
|
enumerable: true,
|
|
110
|
-
get: function () { return
|
|
110
|
+
get: function () { return chunkKRNWMUBN_js.isPaymentStatusSuccess; }
|
|
111
111
|
});
|
|
112
112
|
Object.defineProperty(exports, "isTaxInclusive", {
|
|
113
113
|
enumerable: true,
|
|
114
|
-
get: function () { return
|
|
114
|
+
get: function () { return chunkKRNWMUBN_js.isTaxInclusive; }
|
|
115
115
|
});
|
|
116
116
|
Object.defineProperty(exports, "normalizePaymentResponse", {
|
|
117
117
|
enumerable: true,
|
|
118
|
-
get: function () { return
|
|
118
|
+
get: function () { return chunkKRNWMUBN_js.normalizePaymentResponse; }
|
|
119
119
|
});
|
|
120
120
|
Object.defineProperty(exports, "normalizeStatusResponse", {
|
|
121
121
|
enumerable: true,
|
|
122
|
-
get: function () { return
|
|
122
|
+
get: function () { return chunkKRNWMUBN_js.normalizeStatusResponse; }
|
|
123
123
|
});
|
|
124
124
|
Object.defineProperty(exports, "parsePrice", {
|
|
125
125
|
enumerable: true,
|
|
126
|
-
get: function () { return
|
|
126
|
+
get: function () { return chunkKRNWMUBN_js.parsePrice; }
|
|
127
127
|
});
|
package/dist/utils.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { isValidPhoneForContext, normalizePhoneToE164, resolvePhoneCountryCode } from './chunk-YOIVZ6OA.mjs';
|
|
2
|
-
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice } from './chunk-
|
|
2
|
+
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice } from './chunk-OANVYH27.mjs';
|
package/package.json
CHANGED
|
@@ -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 { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } 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 { 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(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 [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(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 setSpecialInstructions(\"\");\n }, [product.id]);\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 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 {/* 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 <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={1}\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 } 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 \"../types/subscription\";\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 \"./billing-plan-selector\";\nimport { VolumePricing } from \"./volume-pricing\";\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"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|