import * as React from 'react' import {useState, useCallback} from 'react' import {Product} from '@shopify/shop-minis-platform' import {CheckIcon} from 'lucide-react' import {motion, AnimatePresence} from 'motion/react' import {useErrorToast, useShopNavigation} from '../../hooks' import {useShopCartActions} from '../../internal/useShopCartActions' import {cn} from '../../lib/utils' import {Button} from '../atoms/button' /** * Adds products to Shop's cart with checkmark animation feedback. Users can continue shopping and add more items before checkout. Use this for browsing experiences where customers might want multiple products. For immediate single-product purchases, use `BuyNowButton` instead. * @publicDocs */ export interface AddToCartButtonProps { /** Whether the button is disabled */ disabled?: boolean /** CSS class name */ className?: string /** Button size variant */ size?: 'default' | 'sm' | 'lg' /** The discount codes to apply to the cart */ discountCodes?: string[] /** The GID of the product variant. E.g. `gid://shopify/ProductVariant/456` */ productVariantId: string /** The product to add to the cart */ product?: Product } export function AddToCartButton({ disabled = false, className, size = 'default', productVariantId, discountCodes, product, }: AddToCartButtonProps) { const {addToCart} = useShopCartActions() const {navigateToProduct} = useShopNavigation() const [isAdded, setIsAdded] = useState(false) const timeoutRef = React.useRef(undefined) const {id, referral, variants, selectedVariant} = product ?? {} // Look up the matching variant from `variants` first (productLists/savedProducts // path); otherwise fall back to `selectedVariant` (useProduct/useProducts marketplace path). const matchingVariant = variants?.find(variant => variant.id === productVariantId) ?? (selectedVariant?.id === productVariantId ? selectedVariant : undefined) const variantImageUrl = matchingVariant?.image?.url const isSoldOut = matchingVariant?.availableForSale === false // Referral products only navigate to the PDP — never gate them on stock so a // sold-out selected variant doesn't strand the user without a way to switch. const isDisabled = disabled || (!referral && isSoldOut) const {showErrorToast} = useErrorToast() const handleClick = useCallback(async () => { if (isDisabled) return if (id && referral) { navigateToProduct({ productId: id, }) return } if (isAdded) return try { if (id && productVariantId) { addToCart({ productId: id, productVariantId, quantity: 1, discountCodes, variantImageUrl, }) .then(() => {}) .catch(() => { showErrorToast({message: 'Failed to add to cart'}) }) } // Show success state setIsAdded(true) // Clear any existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current) } // Reset to initial state after delay timeoutRef.current = window.setTimeout(() => { setIsAdded(false) }, 2000) } catch (error) { // Handle error - reset to initial state setIsAdded(false) console.error('Failed to add to cart:', error) } }, [ isDisabled, id, referral, isAdded, navigateToProduct, productVariantId, addToCart, discountCodes, variantImageUrl, showErrorToast, ]) // Cleanup timeout on unmount React.useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current) } } }, []) const getButtonText = () => { if (referral) return 'View product' if (isSoldOut) return 'Sold out' if (isAdded) return 'Added to cart' return 'Add to cart' } const buttonText = getButtonText() return ( ) }