@cimplify/sdk 0.110.2 → 0.111.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/CHANGELOG.md +54 -0
- package/README.md +18 -0
- package/dist/advanced.d.mts +4 -4
- package/dist/advanced.d.ts +4 -4
- package/dist/advanced.js +22 -22
- package/dist/advanced.mjs +3 -3
- package/dist/{catalogue-OTuS5zp-.d.ts → catalogue-1QcoGDO6.d.ts} +2 -2
- package/dist/{catalogue-CPTMyBUz.d.mts → catalogue-FdFQiWrU.d.mts} +2 -2
- package/dist/{chunk-SYEO2WV4.mjs → chunk-42JTCWIE.mjs} +3 -3
- package/dist/{chunk-OMTVH334.js → chunk-6QANXRUM.js} +203 -186
- package/dist/{chunk-6ETDAU62.mjs → chunk-B45X3HPO.mjs} +98 -39
- package/dist/{chunk-MKIJQL5Q.js → chunk-CJVEJPIG.js} +69 -69
- package/dist/{chunk-Q2Y3VMKH.js → chunk-ETZDIISN.js} +151 -151
- package/dist/{chunk-GCKWKLZF.mjs → chunk-FT6GFYJL.mjs} +17 -3
- package/dist/{chunk-A36JRXXH.mjs → chunk-KWZHIPRN.mjs} +26 -9
- package/dist/{chunk-SNNEOS5Z.js → chunk-MHDUUSG6.js} +75 -75
- package/dist/{chunk-UCR5HRYM.js → chunk-NFXCKTE2.js} +17 -3
- package/dist/{chunk-DFZWHNRG.js → chunk-QOCHWR4V.js} +105 -42
- package/dist/{chunk-IZDWAL53.mjs → chunk-UJDQQTBS.mjs} +8 -8
- package/dist/{chunk-BTUSVHIE.mjs → chunk-WLSU5QJT.mjs} +2 -2
- package/dist/{client-dqOhP9Nc.d.ts → client-CChON4-x.d.ts} +2 -2
- package/dist/{client-CX_WR0-J.d.mts → client-EeuGm5gj.d.mts} +2 -2
- package/dist/{elements-DdsRig-2.d.mts → elements-ss9xZgRg.d.mts} +27 -1
- package/dist/{elements-DdsRig-2.d.ts → elements-ss9xZgRg.d.ts} +27 -1
- package/dist/index-BB2gjVvA.d.mts +153 -0
- package/dist/index-BEfmBDAH.d.ts +153 -0
- package/dist/{index-B98Xac8e.d.ts → index-DQK8sTfu.d.ts} +2 -2
- package/dist/{index-DbAN_VQE.d.mts → index-DSVkhRmW.d.mts} +2 -2
- package/dist/index.d.mts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +189 -177
- package/dist/index.mjs +8 -8
- package/dist/mock/cli.mjs +25 -10
- package/dist/mock/library.js +25 -10
- package/dist/mock/library.mjs +25 -10
- package/dist/mock/msw.js +25 -10
- package/dist/mock/msw.mjs +25 -10
- package/dist/{price-BOzexBww.d.mts → price-B-w4PwHm.d.mts} +1 -1
- package/dist/{price-C1h9sPxE.d.ts → price-BEvQFNJv.d.ts} +1 -1
- package/dist/react.d.mts +23 -15
- package/dist/react.d.ts +23 -15
- package/dist/react.js +254 -237
- package/dist/react.mjs +128 -111
- package/dist/{scheduling-D2ajXpMC.d.ts → scheduling-D9WwOxbo.d.ts} +1 -1
- package/dist/{scheduling-DSRbbaZb.d.mts → scheduling-DDmrYCr-.d.mts} +1 -1
- package/dist/{server-D29IIuDV.d.ts → server-BiKwxvnf.d.ts} +2 -2
- package/dist/{server-DQMWPnc8.d.mts → server-DPdtl2dd.d.mts} +2 -2
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +10 -10
- package/dist/server.mjs +4 -4
- package/dist/styles.css +1 -1
- package/dist/testing/msw.d.mts +3 -3
- package/dist/testing/msw.d.ts +3 -3
- package/dist/testing/msw.js +3 -3
- package/dist/testing/msw.mjs +2 -2
- package/dist/testing/suite.d.mts +6 -6
- package/dist/testing/suite.d.ts +6 -6
- package/dist/testing/suite.js +25 -25
- package/dist/testing/suite.mjs +6 -6
- package/dist/testing.d.mts +5 -5
- package/dist/testing.d.ts +5 -5
- package/dist/testing.js +117 -117
- package/dist/testing.mjs +7 -7
- package/dist/utils.d.mts +3 -3
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +44 -32
- package/dist/utils.mjs +2 -2
- package/package.json +5 -3
- package/registry/cart-summary.json +1 -1
- package/registry/chat-widget.json +1 -1
- package/registry/price.json +1 -1
- package/registry/product-customizer.json +1 -1
- package/registry/search-input.json +1 -1
- package/dist/index-6gkHMGAq.d.mts +0 -86
- package/dist/index-B_ByTtIT.d.ts +0 -86
|
@@ -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, 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 {\n getProductCurrency,\n getUnitPriceAtQuantity,\n getVariantUnitPrice,\n parsePrice,\n} from \"@cimplify/sdk\";\nimport { isMultiDayService, serviceBillingMultiplier } from \"../utils/price-basis\";\nimport { formatDuration } from \"./utils/format-duration\";\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 { StaffPicker } from \"@cimplify/sdk/react\";\nimport { BookingSummary } from \"./booking-summary\";\nimport type { Staff } from \"@cimplify/sdk\";\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 { CustomerInputFields } from \"./customer-input-fields\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\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 units?: 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 /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (variantId: string | undefined, variant: VariantView | undefined) => void;\n showSpecialInstructions?: boolean;\n /** Submit button verb; the running total is appended. Defaults to \"Add to Cart\". */\n submitLabel?: string;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\ninterface VariantSelection {\n id?: string;\n variant?: VariantView;\n}\n\nconst REQUIRED_OPTIONS_MESSAGE = \"Please select all required options\";\nconst QUOTE_LOADING_MESSAGE = \"Updating price…\";\nconst QUOTE_UNAVAILABLE_MESSAGE = \"Price is unavailable. Please try again.\";\nconst CUSTOMIZER_VALIDATION_ID = \"cimplify-customizer-validation\";\n\nfunction initialVariantSelection(product: ProductWithDetails): VariantSelection {\n const variant =\n product.variants?.find((candidate) => candidate.is_default) ?? product.variants?.[0];\n return { id: variant?.id, variant };\n}\n\nexport function ProductCustomizer(props: ProductCustomizerProps): React.ReactElement {\n // A product change represents a new customization session. Keying the\n // stateful implementation resets it synchronously and avoids an effect that\n // briefly renders the previous product's variant and quote.\n return <ProductCustomizerContent key={props.product.id} {...props} />;\n}\n\nfunction ProductCustomizerContent({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n submitLabel,\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 [variantSelection, setVariantSelection] = useState<VariantSelection>(() =>\n initialVariantSelection(product),\n );\n const selectedVariantId = variantSelection.id;\n const selectedVariant = variantSelection.variant;\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 [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [unitsCount, setUnitsCount] = useState(1);\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 // Whole-stay services can reserve N interchangeable units (rooms, kayaks,\n // pitches) per booking — mirrors desk's wizard, which gates the input the\n // same way; intraday party size is already the quantity.\n const offersUnits = isService && isMultiDayService(product);\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 // Staff offered for the chosen slot. SlotStaffInfo carries id + name; richer\n // profiles (avatars/bios) would come from a staff roster fetch.\n const slotStaff = useMemo<Staff[]>(\n () =>\n (selectedSlot?.available_staff ?? []).map((member) => ({\n id: member.staff_id,\n name: member.name,\n })),\n [selectedSlot],\n );\n const selectedStaffName = selectedStaffId\n ? (slotStaff.find((member) => member.id === selectedStaffId)?.name ?? null)\n : null;\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(\n new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean)),\n ).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 = getVariantUnitPrice(product, selectedVariant);\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 }, [\n product.default_price,\n selectedVariant,\n selectedAddOnOptions,\n quantity,\n isComposite,\n compositePrice,\n isBundle,\n bundleTotalPrice,\n ]);\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:\n isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [\n product.id,\n quantity,\n selectedVariantId,\n normalizedAddOnOptionIds,\n isBundle,\n bundleSelections,\n isComposite,\n compositeSelections,\n ],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const {\n quote,\n isLoading: isQuoteLoading,\n error: quoteError,\n isExpired: isQuoteExpired,\n } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const currency = quote?.currency ?? getProductCurrency(product);\n const quoteId = quote?.quote_id;\n const quotedUnitPrice = (quote?.quoted_total_price_info ?? quote?.final_price_info)?.final_price;\n const quotedTotalPrice = useMemo(() => {\n if (quotedUnitPrice === undefined || quotedUnitPrice === null) return undefined;\n return parsePrice(quotedUnitPrice) * quantity;\n }, [quotedUnitPrice, quantity]);\n\n const quoteIsReady =\n quoteEnabled && quote !== null && !isQuoteLoading && !quoteError && !isQuoteExpired;\n const validationMessage = !quoteEnabled\n ? REQUIRED_OPTIONS_MESSAGE\n : quoteError\n ? QUOTE_UNAVAILABLE_MESSAGE\n : !quoteIsReady\n ? QUOTE_LOADING_MESSAGE\n : null;\n\n // Services bill by basis × periods × units server-side, not by quantity —\n // mirror billing_multiplier so the preview matches the cart. Both totals\n // above already include × quantity, so divide it back out to the unit\n // price before applying the real multiplier.\n const displayTotalPrice = useMemo(() => {\n const total = quotedTotalPrice ?? localTotalPrice;\n const multiplier = serviceBillingMultiplier(product, {\n quantity,\n units: unitsCount,\n scheduledStart: selectedSlot?.start_time,\n scheduledEnd: selectedSlot?.end_time,\n });\n if (multiplier === null) return total;\n return (total / Math.max(1, quantity)) * multiplier;\n }, [quotedTotalPrice, localTotalPrice, product, quantity, unitsCount, selectedSlot]);\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setVariantSelection({ id: variantId, variant });\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (\n isSubmitting ||\n !quoteIsReady ||\n !quote ||\n quotedUnitPrice === undefined ||\n quotedUnitPrice === null\n ) {\n return;\n }\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n locationId: quote.location_id ?? undefined,\n variantId: selectedVariantId,\n variant: selectedVariant\n ? {\n id: selectedVariant.id,\n name: selectedVariant.name || \"\",\n price_adjustment: selectedVariant.price_adjustment,\n price_info: selectedVariant.price_info,\n }\n : undefined,\n quoteId,\n quotedUnitPrice,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions:\n 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:\n isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n units: unitsCount > 1 ? unitsCount : undefined,\n }\n : {}),\n customerInputs:\n Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : 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 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 }, [\n product,\n quantity,\n selectedVariantId,\n selectedVariant,\n quoteId,\n quotedUnitPrice,\n normalizedAddOnOptionIds,\n selectedAddOnOptions,\n isComposite,\n compositeSelections,\n isBundle,\n bundleSelections,\n selectedBillingPlan,\n customerInputValues,\n specialInstructions,\n isService,\n selectedSlot,\n selectedStaffId,\n unitsCount,\n isSubmitting,\n onAddToCart,\n cart,\n quoteIsReady,\n quote,\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 currency={currency}\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 currency={currency}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => {\n setSelectedSlot(slot);\n setSelectedStaffId(null);\n }}\n participantCount={quantity}\n units={offersUnits ? unitsCount : undefined}\n currency={currency}\n // Without the resolved mode the picker renders intraday slots for\n // whole-stay services — stays need the stay-range treatment.\n schedulingMode={product.scheduling_mode ?? (offersUnits ? \"multi_day\" : \"intraday\")}\n durationUnit={product.duration_unit}\n durationValue={product.duration_value}\n />\n )}\n\n {offersUnits && (\n <div data-cimplify-customizer-units className={classNames?.units}>\n <label className=\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\">\n Units\n </label>\n <QuantitySelector value={unitsCount} onChange={setUnitsCount} min={1} />\n </div>\n )}\n\n {isService && slotStaff.length > 0 && (\n <div data-cimplify-customizer-staff>\n <label className=\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\">\n Choose your specialist\n </label>\n <StaffPicker\n staff={slotStaff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={setSelectedStaffId}\n className=\"flex flex-wrap gap-2\"\n classNames={{\n option:\n \"inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:border-foreground/40 cursor-pointer data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background\",\n avatar: \"w-6 h-6 rounded-full object-cover\",\n }}\n />\n </div>\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n currency={currency}\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 currency={currency}\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 currency={currency}\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 currency={currency}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n currency={currency}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div\n data-cimplify-customizer-deposit\n className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}\n >\n {product.deposit_type === \"fixed\" ? (\n <span>\n Deposit required: <Price amount={product.deposit_amount} currency={currency} />\n </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\n data-cimplify-customizer-allergens\n className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}\n >\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\n data-cimplify-customizer-duration\n className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}\n >\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\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 {isService && selectedSlot && (\n <BookingSummary\n slot={selectedSlot}\n total={displayTotalPrice}\n staffName={selectedStaffName}\n durationMinutes={product.duration_minutes}\n depositType={product.deposit_type}\n depositAmount={product.deposit_amount}\n currency={currency}\n />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {validationMessage && (\n <p\n id={CUSTOMIZER_VALIDATION_ID}\n data-cimplify-customizer-validation\n className={cn(\n \"text-sm mb-3\",\n quoteError ? \"text-destructive\" : \"text-muted-foreground\",\n classNames?.validation,\n )}\n >\n {validationMessage}\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\n data-cimplify-customizer-min-hint\n className=\"text-xs text-muted-foreground text-center\"\n >\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div\n data-cimplify-customizer-unit-price\n className=\"text-sm text-muted-foreground text-right shrink-0\"\n >\n <Price\n amount={getUnitPriceAtQuantity(\n product.quantity_pricing,\n quantity,\n parsePrice(product.default_price),\n )}\n currency={currency}\n className=\"font-medium text-foreground\"\n />{\" \"}\n ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteIsReady}\n aria-describedby={validationMessage ? CUSTOMIZER_VALIDATION_ID : 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 ? (\n \"Added\"\n ) : !quoteEnabled ? (\n (submitLabel ?? \"Add to Cart\")\n ) : !quoteIsReady ? (\n quoteError ? (\n QUOTE_UNAVAILABLE_MESSAGE\n ) : (\n QUOTE_LOADING_MESSAGE\n )\n ) : (\n <>\n {submitLabel ?? \"Add to Cart\"} ·{\" \"}\n <Price amount={displayTotalPrice} currency={currency} />\n </>\n )}\n </Button>\n </div>\n </div>\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 {\n getProductCurrency,\n getUnitPriceAtQuantity,\n getVariantUnitPrice,\n parsePrice,\n} from \"@cimplify/sdk\";\nimport { isMultiDayService, serviceBillingMultiplier } from \"../utils/price-basis\";\nimport { formatDuration } from \"./utils/format-duration\";\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 { StaffPicker } from \"@cimplify/sdk/react\";\nimport { BookingSummary } from \"./booking-summary\";\nimport type { Staff } from \"@cimplify/sdk\";\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 { CustomerInputFields } from \"./customer-input-fields\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\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 units?: 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 /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (variantId: string | undefined, variant: VariantView | undefined) => void;\n showSpecialInstructions?: boolean;\n /** Submit button verb; the running total is appended. Defaults to \"Add to Cart\". */\n submitLabel?: string;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\ninterface VariantSelection {\n id?: string;\n variant?: VariantView;\n}\n\nconst REQUIRED_OPTIONS_MESSAGE = \"Please select all required options\";\nconst QUOTE_LOADING_MESSAGE = \"Updating price…\";\nconst QUOTE_UNAVAILABLE_MESSAGE = \"Price is unavailable. Please try again.\";\nconst CUSTOMIZER_VALIDATION_ID = \"cimplify-customizer-validation\";\n\nfunction initialVariantSelection(product: ProductWithDetails): VariantSelection {\n const variant =\n product.variants?.find((candidate) => candidate.is_default) ?? product.variants?.[0];\n return { id: variant?.id, variant };\n}\n\nexport function ProductCustomizer(props: ProductCustomizerProps): React.ReactElement {\n // A product change represents a new customization session. Keying the\n // stateful implementation resets it synchronously and avoids an effect that\n // briefly renders the previous product's variant and quote.\n return <ProductCustomizerContent key={props.product.id} {...props} />;\n}\n\nfunction ProductCustomizerContent({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n submitLabel,\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 [variantSelection, setVariantSelection] = useState<VariantSelection>(() =>\n initialVariantSelection(product),\n );\n const selectedVariantId = variantSelection.id;\n const selectedVariant = variantSelection.variant;\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 [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n const [unitsCount, setUnitsCount] = useState(1);\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 // Whole-stay services can reserve N interchangeable units (rooms, kayaks,\n // pitches) per booking — mirrors desk's wizard, which gates the input the\n // same way; intraday party size is already the quantity.\n const offersUnits = isService && isMultiDayService(product);\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 // Staff offered for the chosen slot. SlotStaffInfo carries id + name; richer\n // profiles (avatars/bios) would come from a staff roster fetch.\n const slotStaff = useMemo<Staff[]>(\n () =>\n (selectedSlot?.available_staff ?? []).map((member) => ({\n id: member.staff_id,\n name: member.name,\n })),\n [selectedSlot],\n );\n const selectedStaffName = selectedStaffId\n ? (slotStaff.find((member) => member.id === selectedStaffId)?.name ?? null)\n : null;\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(\n new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean)),\n ).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 = getVariantUnitPrice(product, selectedVariant);\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 }, [\n product.default_price,\n selectedVariant,\n selectedAddOnOptions,\n quantity,\n isComposite,\n compositePrice,\n isBundle,\n bundleTotalPrice,\n ]);\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:\n isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [\n product.id,\n quantity,\n selectedVariantId,\n normalizedAddOnOptionIds,\n isBundle,\n bundleSelections,\n isComposite,\n compositeSelections,\n ],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const {\n quote,\n isLoading: isQuoteLoading,\n error: quoteError,\n isExpired: isQuoteExpired,\n } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quotedPriceInfo = quote?.quoted_total_price_info ?? quote?.final_price_info;\n const currency = quote?.currency ?? quotedPriceInfo?.currency ?? getProductCurrency(product);\n const quoteId = quote?.quote_id;\n const quotedUnitPrice = quotedPriceInfo?.final_price;\n const quotedTotalPrice = useMemo(() => {\n if (quotedUnitPrice === undefined || quotedUnitPrice === null) return undefined;\n return parsePrice(quotedUnitPrice) * quantity;\n }, [quotedUnitPrice, quantity]);\n\n const quoteIsReady =\n quoteEnabled && quote !== null && !isQuoteLoading && !quoteError && !isQuoteExpired;\n const validationMessage = !quoteEnabled\n ? REQUIRED_OPTIONS_MESSAGE\n : quoteError\n ? QUOTE_UNAVAILABLE_MESSAGE\n : !quoteIsReady\n ? QUOTE_LOADING_MESSAGE\n : null;\n\n // Services bill by basis × periods × units server-side, not by quantity —\n // mirror billing_multiplier so the preview matches the cart. Both totals\n // above already include × quantity, so divide it back out to the unit\n // price before applying the real multiplier.\n const displayTotalPrice = useMemo(() => {\n const total = quotedTotalPrice ?? localTotalPrice;\n const multiplier = serviceBillingMultiplier(product, {\n quantity,\n units: unitsCount,\n scheduledStart: selectedSlot?.start_time,\n scheduledEnd: selectedSlot?.end_time,\n });\n if (multiplier === null) return total;\n return (total / Math.max(1, quantity)) * multiplier;\n }, [quotedTotalPrice, localTotalPrice, product, quantity, unitsCount, selectedSlot]);\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setVariantSelection({ id: variantId, variant });\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (\n isSubmitting ||\n !quoteIsReady ||\n !quote ||\n quotedUnitPrice === undefined ||\n quotedUnitPrice === null\n ) {\n return;\n }\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n locationId: quote.location_id ?? undefined,\n variantId: selectedVariantId,\n variant: selectedVariant\n ? {\n id: selectedVariant.id,\n name: selectedVariant.name || \"\",\n price_adjustment: selectedVariant.price_adjustment,\n price_info: selectedVariant.price_info,\n }\n : undefined,\n quoteId,\n quotedUnitPrice,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions:\n 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:\n isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n units: unitsCount > 1 ? unitsCount : undefined,\n }\n : {}),\n customerInputs:\n Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : 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 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 }, [\n product,\n quantity,\n selectedVariantId,\n selectedVariant,\n quoteId,\n quotedUnitPrice,\n normalizedAddOnOptionIds,\n selectedAddOnOptions,\n isComposite,\n compositeSelections,\n isBundle,\n bundleSelections,\n selectedBillingPlan,\n customerInputValues,\n specialInstructions,\n isService,\n selectedSlot,\n selectedStaffId,\n unitsCount,\n isSubmitting,\n onAddToCart,\n cart,\n quoteIsReady,\n quote,\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 currency={currency}\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 currency={currency}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => {\n setSelectedSlot(slot);\n setSelectedStaffId(null);\n }}\n participantCount={quantity}\n units={offersUnits ? unitsCount : undefined}\n currency={currency}\n // Without the resolved mode the picker renders intraday slots for\n // whole-stay services — stays need the stay-range treatment.\n schedulingMode={product.scheduling_mode ?? (offersUnits ? \"multi_day\" : \"intraday\")}\n durationUnit={product.duration_unit}\n durationValue={product.duration_value}\n />\n )}\n\n {offersUnits && (\n <div data-cimplify-customizer-units className={classNames?.units}>\n <label className=\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\">\n Units\n </label>\n <QuantitySelector value={unitsCount} onChange={setUnitsCount} min={1} />\n </div>\n )}\n\n {isService && slotStaff.length > 0 && (\n <div data-cimplify-customizer-staff>\n <label className=\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\">\n Choose your specialist\n </label>\n <StaffPicker\n staff={slotStaff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={setSelectedStaffId}\n className=\"flex flex-wrap gap-2\"\n classNames={{\n option:\n \"inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:border-foreground/40 cursor-pointer data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background\",\n avatar: \"w-6 h-6 rounded-full object-cover\",\n }}\n />\n </div>\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n currency={currency}\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 currency={currency}\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 currency={currency}\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 currency={currency}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n currency={currency}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div\n data-cimplify-customizer-deposit\n className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}\n >\n {product.deposit_type === \"fixed\" ? (\n <span>\n Deposit required: <Price amount={product.deposit_amount} currency={currency} />\n </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\n data-cimplify-customizer-allergens\n className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}\n >\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\n data-cimplify-customizer-duration\n className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}\n >\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\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 {isService && selectedSlot && (\n <BookingSummary\n slot={selectedSlot}\n total={displayTotalPrice}\n staffName={selectedStaffName}\n durationMinutes={product.duration_minutes}\n depositType={product.deposit_type}\n depositAmount={product.deposit_amount}\n currency={currency}\n />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {validationMessage && (\n <p\n id={CUSTOMIZER_VALIDATION_ID}\n data-cimplify-customizer-validation\n className={cn(\n \"text-sm mb-3\",\n quoteError ? \"text-destructive\" : \"text-muted-foreground\",\n classNames?.validation,\n )}\n >\n {validationMessage}\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\n data-cimplify-customizer-min-hint\n className=\"text-xs text-muted-foreground text-center\"\n >\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div\n data-cimplify-customizer-unit-price\n className=\"text-sm text-muted-foreground text-right shrink-0\"\n >\n <Price\n amount={getUnitPriceAtQuantity(\n product.quantity_pricing,\n quantity,\n parsePrice(product.default_price),\n )}\n currency={currency}\n className=\"font-medium text-foreground\"\n />{\" \"}\n ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteIsReady}\n aria-describedby={validationMessage ? CUSTOMIZER_VALIDATION_ID : 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 ? (\n \"Added\"\n ) : !quoteEnabled ? (\n (submitLabel ?? \"Add to Cart\")\n ) : !quoteIsReady ? (\n quoteError ? (\n QUOTE_UNAVAILABLE_MESSAGE\n ) : (\n QUOTE_LOADING_MESSAGE\n )\n ) : (\n <>\n {submitLabel ?? \"Add to Cart\"} ·{\" \"}\n <Price amount={displayTotalPrice} currency={currency} />\n </>\n )}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
{
|
|
11
11
|
"path": "search-input.tsx",
|
|
12
|
-
"content": "\"use client\";\n\nimport React, { useCallback, useMemo } from \"react\";\nimport { Field } from \"@base-ui/react/field\";\nimport { Input } from \"@base-ui/react/input\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport type { UseSearchOptions } from \"@cimplify/sdk/react\";\nimport { prefetchProduct } from \"./hooks/use-product\";\nimport { speculate } from \"./utils/prefetch-policy\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport {
|
|
12
|
+
"content": "\"use client\";\n\nimport React, { useCallback, useMemo } from \"react\";\nimport { Field } from \"@base-ui/react/field\";\nimport { Input } from \"@base-ui/react/input\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { useSearch } from \"@cimplify/sdk/react\";\nimport type { UseSearchOptions } from \"@cimplify/sdk/react\";\nimport { prefetchProduct } from \"./hooks/use-product\";\nimport { speculate } from \"./utils/prefetch-policy\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\nimport { getProductCurrency } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\n\nexport interface SearchInputClassNames {\n root?: string;\n input?: string;\n clearButton?: string;\n results?: string;\n resultItem?: string;\n empty?: string;\n loading?: string;\n resultImage?: string;\n resultName?: string;\n resultPrice?: string;\n categoryFilter?: string;\n}\n\nexport interface SearchInputProps {\n /** Placeholder text for the input. */\n placeholder?: string;\n /** Search options forwarded to useSearch. */\n searchOptions?: UseSearchOptions;\n /** Called when a product result is clicked. */\n onResultClick?: (product: Product) => void;\n /** Custom result item renderer. */\n renderResult?: (product: Product) => React.ReactNode;\n /** Show inline results dropdown. Default: true. */\n showResults?: boolean;\n /** Optional category ID to scope search results. */\n categoryId?: string;\n /** Display name for the active category filter. */\n categoryName?: string;\n className?: string;\n classNames?: SearchInputClassNames;\n}\n\nconst SearchIcon = () => (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n data-cimplify-search-icon\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" />\n </svg>\n);\n\n/**\n * SearchInput — search bar with debounced results dropdown.\n *\n * Wraps `useSearch` with a controlled input and optional inline results list.\n * Uses Base UI Field.Root + Input for accessible form semantics.\n */\nexport function SearchInput({\n placeholder = \"Search products...\",\n searchOptions,\n onResultClick,\n renderResult,\n showResults = true,\n categoryId,\n categoryName,\n className,\n classNames,\n}: SearchInputProps): React.ReactElement {\n const mergedOptions = useMemo<UseSearchOptions>(\n () => (categoryId ? { ...searchOptions, category: categoryId } : (searchOptions ?? {})),\n [searchOptions, categoryId],\n );\n const { results, isLoading, query, setQuery, clear } = useSearch(mergedOptions);\n\n const context = useOptionalCimplifyClient();\n const warmResult = useCallback(\n (product: Product) => {\n const client = mergedOptions.client ?? context?.client;\n if (!client || context?.prefetch.mode === \"off\") return;\n speculate(() => prefetchProduct(client, product.slug ?? product.id));\n },\n [mergedOptions.client, context],\n );\n\n const handleValueChange = useCallback(\n (value: string) => {\n setQuery(value);\n },\n [setQuery],\n );\n\n return (\n <Field.Root\n data-cimplify-search\n className={cn(className, classNames?.root)}\n style={{ position: \"relative\" }}\n >\n <Field.Label className=\"sr-only\">Search products</Field.Label>\n\n <div style={{ position: \"relative\", display: \"flex\", alignItems: \"center\" }}>\n <span\n data-cimplify-search-icon-wrapper\n style={{\n position: \"absolute\",\n left: \"0.5rem\",\n pointerEvents: \"none\",\n display: \"flex\",\n alignItems: \"center\",\n }}\n >\n <SearchIcon />\n </span>\n\n <Input\n type=\"search\"\n value={query}\n onValueChange={handleValueChange}\n placeholder={placeholder}\n data-cimplify-search-input\n className={classNames?.input}\n />\n\n {query.length > 0 && (\n <button\n type=\"button\"\n onClick={clear}\n data-cimplify-search-clear\n className={classNames?.clearButton}\n aria-label=\"Clear search\"\n >\n ×\n </button>\n )}\n </div>\n\n {categoryId && (\n <div data-cimplify-search-category-filter className={classNames?.categoryFilter}>\n Filtered by: {categoryName || categoryId}\n </div>\n )}\n\n {showResults && query.length > 0 && (\n <div data-cimplify-search-results className={classNames?.results}>\n {isLoading && (\n <div data-cimplify-search-loading className={classNames?.loading} aria-busy=\"true\">\n Searching...\n </div>\n )}\n\n {!isLoading && results.length === 0 && query.length >= 2 && (\n <div data-cimplify-search-empty className={classNames?.empty}>\n No results found\n </div>\n )}\n\n {!isLoading &&\n results.map((product) => (\n <button\n key={product.id}\n type=\"button\"\n onClick={() => onResultClick?.(product)}\n onPointerEnter={() => warmResult(product)}\n onFocus={() => warmResult(product)}\n data-cimplify-search-result\n className={classNames?.resultItem}\n style={\n !renderResult\n ? { display: \"flex\", alignItems: \"center\", gap: \"0.5rem\", width: \"100%\" }\n : undefined\n }\n >\n {renderResult ? (\n renderResult(product)\n ) : (\n <>\n {(product.images?.[0] || product.image_url) && (\n <img\n src={product.images?.[0] || product.image_url}\n alt=\"\"\n data-cimplify-search-result-image\n className={classNames?.resultImage}\n style={{\n width: \"2rem\",\n height: \"2rem\",\n objectFit: \"cover\",\n borderRadius: \"0.25rem\",\n flexShrink: 0,\n }}\n />\n )}\n <span\n data-cimplify-search-result-name\n className={classNames?.resultName}\n style={{ flex: 1, textAlign: \"left\" }}\n >\n {product.name}\n </span>\n {product.default_price !== undefined && (\n <span\n data-cimplify-search-result-price\n className={classNames?.resultPrice}\n style={{ flexShrink: 0 }}\n >\n <Price\n amount={product.default_price}\n currency={getProductCurrency(product)}\n />\n </span>\n )}\n </>\n )}\n </button>\n ))}\n </div>\n )}\n </Field.Root>\n );\n}\n"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { M as Money, C as CurrencyCode, dL as PricePathTaxInfo, P as Product, f as ProductWithDetails, el as QuantityPricingTier, cN as MobileMoneyProvider, dq as PaymentErrorDetails, dA as PaymentResponse, dC as PaymentStatusResponse, ea as PurchaseIntentMobileMoneyNetwork } from './elements-DdsRig-2.mjs';
|
|
2
|
-
import { P as ProductWithPrice } from './price-BOzexBww.mjs';
|
|
3
|
-
import { CountryCode } from 'libphonenumber-js';
|
|
4
|
-
|
|
5
|
-
declare const CURRENCY_SYMBOLS: Record<string, string>;
|
|
6
|
-
declare function getCurrencySymbol(currencyCode: CurrencyCode): string;
|
|
7
|
-
declare function formatNumberCompact(value: number, decimals?: number): string;
|
|
8
|
-
declare function formatPrice(amount: number | Money, currency?: CurrencyCode, locale?: string): string;
|
|
9
|
-
declare function formatPriceAdjustment(amount: number, currency?: CurrencyCode, locale?: string): string;
|
|
10
|
-
declare function formatPriceCompact(amount: number | Money, currency?: CurrencyCode, decimals?: number): string;
|
|
11
|
-
declare function formatMoney(amount: Money | number, currency?: CurrencyCode): string;
|
|
12
|
-
declare function parsePrice(value: Money | string | number | undefined | null): number;
|
|
13
|
-
declare function hasTaxInfo(priceInfo: {
|
|
14
|
-
tax_info?: PricePathTaxInfo;
|
|
15
|
-
}): boolean;
|
|
16
|
-
declare function getTaxAmount(priceInfo: {
|
|
17
|
-
tax_info?: PricePathTaxInfo;
|
|
18
|
-
}): number;
|
|
19
|
-
declare function isTaxInclusive(priceInfo: {
|
|
20
|
-
tax_info?: PricePathTaxInfo;
|
|
21
|
-
}): boolean;
|
|
22
|
-
declare function formatPriceWithTax(priceInfo: {
|
|
23
|
-
final_price: Money;
|
|
24
|
-
tax_info?: PricePathTaxInfo;
|
|
25
|
-
}, currency?: CurrencyCode): string;
|
|
26
|
-
declare function getDisplayPrice(product: ProductWithPrice): number;
|
|
27
|
-
declare function getBasePrice(product: ProductWithPrice): number;
|
|
28
|
-
declare function isOnSale(product: ProductWithPrice): boolean;
|
|
29
|
-
declare function getDiscountPercentage(product: ProductWithPrice): number;
|
|
30
|
-
declare function getMarkupPercentage(product: ProductWithPrice): number;
|
|
31
|
-
declare function getProductCurrency(product: ProductWithPrice): CurrencyCode;
|
|
32
|
-
declare function formatProductPrice(product: ProductWithPrice, locale?: string): string;
|
|
33
|
-
declare function getUnitPriceAtQuantity(tiers: QuantityPricingTier[], quantity: number, fallback: number): number;
|
|
34
|
-
declare function getPriceRange(product: Product | ProductWithDetails): {
|
|
35
|
-
min: number;
|
|
36
|
-
max: number;
|
|
37
|
-
} | null;
|
|
38
|
-
declare function formatPriceRange(min: number, max: number, currency?: CurrencyCode): string;
|
|
39
|
-
|
|
40
|
-
declare function categorizePaymentError(error: Error, errorCode?: string): PaymentErrorDetails;
|
|
41
|
-
declare function normalizePaymentResponse(response: unknown): PaymentResponse;
|
|
42
|
-
declare const PAYMENT_TERMINAL_FAILURE_STATUSES: readonly ["failed", "cancelled", "voided", "abandoned", "refunded", "refund_pending", "reversed", "disputed"];
|
|
43
|
-
declare function isPaymentStatusSuccess(status: string | undefined): boolean;
|
|
44
|
-
declare function isPaymentStatusFailure(status: string | undefined): boolean;
|
|
45
|
-
declare function isPaymentStatusRequiresAction(status: string | undefined): boolean;
|
|
46
|
-
declare function normalizeStatusResponse(response: unknown): PaymentStatusResponse;
|
|
47
|
-
declare const MOBILE_MONEY_PROVIDERS: {
|
|
48
|
-
readonly mtn: {
|
|
49
|
-
readonly name: "MTN Mobile Money";
|
|
50
|
-
readonly prefix: readonly ["024", "025", "053", "054", "055", "059"];
|
|
51
|
-
};
|
|
52
|
-
readonly vodafone: {
|
|
53
|
-
readonly name: "Telecel Cash";
|
|
54
|
-
readonly prefix: readonly ["020", "050"];
|
|
55
|
-
};
|
|
56
|
-
readonly airtel: {
|
|
57
|
-
readonly name: "AirtelTigo Money";
|
|
58
|
-
readonly prefix: readonly ["026", "027", "056", "057"];
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
type CanonicalMobileMoneyProvider = Extract<MobileMoneyProvider, "mtn" | "telecel" | "airteltigo" | "mpesa">;
|
|
62
|
-
/** Normalize route-catalog and legacy network aliases to storefront provider codes. */
|
|
63
|
-
declare function canonicalMobileMoneyProvider(provider: string | null | undefined): CanonicalMobileMoneyProvider | null;
|
|
64
|
-
/**
|
|
65
|
-
* Collapse storefront aliases to Checkout's closed durable network vocabulary.
|
|
66
|
-
* M-PESA is deliberately absent until the backend can issue a matching
|
|
67
|
-
* PurchaseIntent payment option.
|
|
68
|
-
*/
|
|
69
|
-
declare function purchaseIntentMobileMoneyNetwork(provider: string | null | undefined): PurchaseIntentMobileMoneyNetwork | null;
|
|
70
|
-
/**
|
|
71
|
-
* Validate an external payment-authorization URL before browser navigation.
|
|
72
|
-
* Provider payloads are data, not executable navigation instructions: only
|
|
73
|
-
* HTTPS is accepted outside local development.
|
|
74
|
-
*/
|
|
75
|
-
declare function paymentAuthorizationUrl(value: unknown): string | null;
|
|
76
|
-
declare function detectMobileMoneyProvider(phoneNumber: string): "mtn" | "vodafone" | "airtel" | null;
|
|
77
|
-
|
|
78
|
-
interface PhoneNormalizationOptions {
|
|
79
|
-
country?: string | null;
|
|
80
|
-
currency?: string | null;
|
|
81
|
-
}
|
|
82
|
-
declare function resolvePhoneCountryCode(options?: PhoneNormalizationOptions): CountryCode | undefined;
|
|
83
|
-
declare function normalizePhoneToE164(raw: string, options?: PhoneNormalizationOptions): string | null;
|
|
84
|
-
declare function isValidPhoneForContext(raw: string, options?: PhoneNormalizationOptions): boolean;
|
|
85
|
-
|
|
86
|
-
export { isTaxInclusive as A, isValidPhoneForContext as B, CURRENCY_SYMBOLS as C, normalizePaymentResponse as D, normalizePhoneToE164 as E, normalizeStatusResponse as F, parsePrice as G, paymentAuthorizationUrl as H, purchaseIntentMobileMoneyNetwork as I, resolvePhoneCountryCode as J, MOBILE_MONEY_PROVIDERS as M, PAYMENT_TERMINAL_FAILURE_STATUSES as P, type CanonicalMobileMoneyProvider as a, categorizePaymentError as b, canonicalMobileMoneyProvider as c, detectMobileMoneyProvider as d, formatNumberCompact as e, formatMoney as f, formatPrice as g, formatPriceAdjustment as h, formatPriceCompact as i, formatPriceRange as j, formatPriceWithTax as k, formatProductPrice as l, getBasePrice as m, getCurrencySymbol as n, getDiscountPercentage as o, getDisplayPrice as p, getMarkupPercentage as q, getPriceRange as r, getProductCurrency as s, getTaxAmount as t, getUnitPriceAtQuantity as u, hasTaxInfo as v, isOnSale as w, isPaymentStatusFailure as x, isPaymentStatusRequiresAction as y, isPaymentStatusSuccess as z };
|
package/dist/index-B_ByTtIT.d.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { M as Money, C as CurrencyCode, dL as PricePathTaxInfo, P as Product, f as ProductWithDetails, el as QuantityPricingTier, cN as MobileMoneyProvider, dq as PaymentErrorDetails, dA as PaymentResponse, dC as PaymentStatusResponse, ea as PurchaseIntentMobileMoneyNetwork } from './elements-DdsRig-2.js';
|
|
2
|
-
import { P as ProductWithPrice } from './price-C1h9sPxE.js';
|
|
3
|
-
import { CountryCode } from 'libphonenumber-js';
|
|
4
|
-
|
|
5
|
-
declare const CURRENCY_SYMBOLS: Record<string, string>;
|
|
6
|
-
declare function getCurrencySymbol(currencyCode: CurrencyCode): string;
|
|
7
|
-
declare function formatNumberCompact(value: number, decimals?: number): string;
|
|
8
|
-
declare function formatPrice(amount: number | Money, currency?: CurrencyCode, locale?: string): string;
|
|
9
|
-
declare function formatPriceAdjustment(amount: number, currency?: CurrencyCode, locale?: string): string;
|
|
10
|
-
declare function formatPriceCompact(amount: number | Money, currency?: CurrencyCode, decimals?: number): string;
|
|
11
|
-
declare function formatMoney(amount: Money | number, currency?: CurrencyCode): string;
|
|
12
|
-
declare function parsePrice(value: Money | string | number | undefined | null): number;
|
|
13
|
-
declare function hasTaxInfo(priceInfo: {
|
|
14
|
-
tax_info?: PricePathTaxInfo;
|
|
15
|
-
}): boolean;
|
|
16
|
-
declare function getTaxAmount(priceInfo: {
|
|
17
|
-
tax_info?: PricePathTaxInfo;
|
|
18
|
-
}): number;
|
|
19
|
-
declare function isTaxInclusive(priceInfo: {
|
|
20
|
-
tax_info?: PricePathTaxInfo;
|
|
21
|
-
}): boolean;
|
|
22
|
-
declare function formatPriceWithTax(priceInfo: {
|
|
23
|
-
final_price: Money;
|
|
24
|
-
tax_info?: PricePathTaxInfo;
|
|
25
|
-
}, currency?: CurrencyCode): string;
|
|
26
|
-
declare function getDisplayPrice(product: ProductWithPrice): number;
|
|
27
|
-
declare function getBasePrice(product: ProductWithPrice): number;
|
|
28
|
-
declare function isOnSale(product: ProductWithPrice): boolean;
|
|
29
|
-
declare function getDiscountPercentage(product: ProductWithPrice): number;
|
|
30
|
-
declare function getMarkupPercentage(product: ProductWithPrice): number;
|
|
31
|
-
declare function getProductCurrency(product: ProductWithPrice): CurrencyCode;
|
|
32
|
-
declare function formatProductPrice(product: ProductWithPrice, locale?: string): string;
|
|
33
|
-
declare function getUnitPriceAtQuantity(tiers: QuantityPricingTier[], quantity: number, fallback: number): number;
|
|
34
|
-
declare function getPriceRange(product: Product | ProductWithDetails): {
|
|
35
|
-
min: number;
|
|
36
|
-
max: number;
|
|
37
|
-
} | null;
|
|
38
|
-
declare function formatPriceRange(min: number, max: number, currency?: CurrencyCode): string;
|
|
39
|
-
|
|
40
|
-
declare function categorizePaymentError(error: Error, errorCode?: string): PaymentErrorDetails;
|
|
41
|
-
declare function normalizePaymentResponse(response: unknown): PaymentResponse;
|
|
42
|
-
declare const PAYMENT_TERMINAL_FAILURE_STATUSES: readonly ["failed", "cancelled", "voided", "abandoned", "refunded", "refund_pending", "reversed", "disputed"];
|
|
43
|
-
declare function isPaymentStatusSuccess(status: string | undefined): boolean;
|
|
44
|
-
declare function isPaymentStatusFailure(status: string | undefined): boolean;
|
|
45
|
-
declare function isPaymentStatusRequiresAction(status: string | undefined): boolean;
|
|
46
|
-
declare function normalizeStatusResponse(response: unknown): PaymentStatusResponse;
|
|
47
|
-
declare const MOBILE_MONEY_PROVIDERS: {
|
|
48
|
-
readonly mtn: {
|
|
49
|
-
readonly name: "MTN Mobile Money";
|
|
50
|
-
readonly prefix: readonly ["024", "025", "053", "054", "055", "059"];
|
|
51
|
-
};
|
|
52
|
-
readonly vodafone: {
|
|
53
|
-
readonly name: "Telecel Cash";
|
|
54
|
-
readonly prefix: readonly ["020", "050"];
|
|
55
|
-
};
|
|
56
|
-
readonly airtel: {
|
|
57
|
-
readonly name: "AirtelTigo Money";
|
|
58
|
-
readonly prefix: readonly ["026", "027", "056", "057"];
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
type CanonicalMobileMoneyProvider = Extract<MobileMoneyProvider, "mtn" | "telecel" | "airteltigo" | "mpesa">;
|
|
62
|
-
/** Normalize route-catalog and legacy network aliases to storefront provider codes. */
|
|
63
|
-
declare function canonicalMobileMoneyProvider(provider: string | null | undefined): CanonicalMobileMoneyProvider | null;
|
|
64
|
-
/**
|
|
65
|
-
* Collapse storefront aliases to Checkout's closed durable network vocabulary.
|
|
66
|
-
* M-PESA is deliberately absent until the backend can issue a matching
|
|
67
|
-
* PurchaseIntent payment option.
|
|
68
|
-
*/
|
|
69
|
-
declare function purchaseIntentMobileMoneyNetwork(provider: string | null | undefined): PurchaseIntentMobileMoneyNetwork | null;
|
|
70
|
-
/**
|
|
71
|
-
* Validate an external payment-authorization URL before browser navigation.
|
|
72
|
-
* Provider payloads are data, not executable navigation instructions: only
|
|
73
|
-
* HTTPS is accepted outside local development.
|
|
74
|
-
*/
|
|
75
|
-
declare function paymentAuthorizationUrl(value: unknown): string | null;
|
|
76
|
-
declare function detectMobileMoneyProvider(phoneNumber: string): "mtn" | "vodafone" | "airtel" | null;
|
|
77
|
-
|
|
78
|
-
interface PhoneNormalizationOptions {
|
|
79
|
-
country?: string | null;
|
|
80
|
-
currency?: string | null;
|
|
81
|
-
}
|
|
82
|
-
declare function resolvePhoneCountryCode(options?: PhoneNormalizationOptions): CountryCode | undefined;
|
|
83
|
-
declare function normalizePhoneToE164(raw: string, options?: PhoneNormalizationOptions): string | null;
|
|
84
|
-
declare function isValidPhoneForContext(raw: string, options?: PhoneNormalizationOptions): boolean;
|
|
85
|
-
|
|
86
|
-
export { isTaxInclusive as A, isValidPhoneForContext as B, CURRENCY_SYMBOLS as C, normalizePaymentResponse as D, normalizePhoneToE164 as E, normalizeStatusResponse as F, parsePrice as G, paymentAuthorizationUrl as H, purchaseIntentMobileMoneyNetwork as I, resolvePhoneCountryCode as J, MOBILE_MONEY_PROVIDERS as M, PAYMENT_TERMINAL_FAILURE_STATUSES as P, type CanonicalMobileMoneyProvider as a, categorizePaymentError as b, canonicalMobileMoneyProvider as c, detectMobileMoneyProvider as d, formatNumberCompact as e, formatMoney as f, formatPrice as g, formatPriceAdjustment as h, formatPriceCompact as i, formatPriceRange as j, formatPriceWithTax as k, formatProductPrice as l, getBasePrice as m, getCurrencySymbol as n, getDiscountPercentage as o, getDisplayPrice as p, getMarkupPercentage as q, getPriceRange as r, getProductCurrency as s, getTaxAmount as t, getUnitPriceAtQuantity as u, hasTaxInfo as v, isOnSale as w, isPaymentStatusFailure as x, isPaymentStatusRequiresAction as y, isPaymentStatusSuccess as z };
|