@replohq/sdk 1.5.0 → 1.7.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/_vendor/replo-utils/lib/url.d.ts +5 -1
- package/cart/cart-actions.js +2 -1
- package/cart/cart-actions.js.map +2 -2
- package/cart/cart-provider.d.ts +39 -3
- package/cart/cart-provider.js +187 -119
- package/cart/cart-provider.js.map +3 -3
- package/cart/cart-types.d.ts +15 -0
- package/cart/cart-types.js.map +1 -1
- package/cart/gateways/cart-gateway.d.ts +7 -0
- package/cart/gateways/cart-gateway.js.map +2 -2
- package/cart/gateways/cookie-cart-gateway.d.ts +9 -0
- package/cart/gateways/cookie-cart-gateway.js +54 -3
- package/cart/gateways/cookie-cart-gateway.js.map +2 -2
- package/cart/gateways/resolver-cart-gateway.js +4 -0
- package/cart/gateways/resolver-cart-gateway.js.map +2 -2
- package/cart/gateways/stripe-cart-gateway.d.ts +14 -0
- package/cart/gateways/stripe-cart-gateway.js +6 -1
- package/cart/gateways/stripe-cart-gateway.js.map +2 -2
- package/cart/hooks/use-add-to-cart.d.ts +22 -3
- package/cart/hooks/use-add-to-cart.js +8 -1
- package/cart/hooks/use-add-to-cart.js.map +2 -2
- package/cart/utils/cart-adjustments.d.ts +29 -0
- package/cart/utils/cart-adjustments.js +81 -0
- package/cart/utils/cart-adjustments.js.map +7 -0
- package/cart/utils/cart-error.d.ts +5 -0
- package/cart/utils/cart-error.js +20 -0
- package/cart/utils/cart-error.js.map +7 -0
- package/cart/utils/cart-utils.d.ts +14 -0
- package/cart/utils/cart-utils.js +43 -0
- package/cart/utils/cart-utils.js.map +2 -2
- package/cart/utils/cookie-cart-persistence.d.ts +4 -2
- package/cart/utils/cookie-cart-persistence.js +35 -21
- package/cart/utils/cookie-cart-persistence.js.map +3 -3
- package/gateways/canopy-api.d.ts +25 -0
- package/gateways/canopy-api.js +40 -0
- package/gateways/canopy-api.js.map +2 -2
- package/lib/buildMetadata.js +3 -3
- package/loaders/loader-schemas.d.ts +5 -0
- package/loaders/loader-schemas.js +2 -0
- package/loaders/loader-schemas.js.map +2 -2
- package/loaders/rebuy-recommendations-loader.d.ts +1 -0
- package/next/config.d.ts +18 -0
- package/next/config.js.map +2 -2
- package/package.json +4 -4
- package/providers/replo-provider.d.ts +19 -0
- package/providers/replo-provider.js.map +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/cart-provider.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport {\n buildCartLineAnalyticsData,\n buildCartViewAnalyticsData,\n} from \"./utils/cart-analytics\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart> | null = null;\n\nfunction getOrCreateSharedCart() {\n cartCreationPromise ??= getOrCreateCartAction()\n .then((cart) => {\n if (!cart) {\n throw new CartContextError({\n message: \"Your cart could not be loaded. Please try again.\",\n });\n }\n return cart;\n })\n .finally(() => {\n cartCreationPromise = null;\n });\n return cartCreationPromise;\n}\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart({\n cart,\n line,\n lineId,\n}: {\n cart: Cart;\n line: CartLinePayload;\n lineId: string;\n}): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: lineId,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const optimisticCart = React.useRef(cart);\n const updateCartData = React.useCallback(\n (update: React.SetStateAction<Cart | null>) => {\n const nextCart =\n typeof update === \"function\" ? update(optimisticCart.current) : update;\n // Analytics and overlapping mutations need the snapshot before React renders it.\n optimisticCart.current = nextCart;\n setCartData(nextCart);\n return nextCart;\n },\n [],\n );\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const isMounted = React.useRef(true);\n const analytics = useAnalyticsOptional();\n const serverCart = React.useRef(cart);\n const needsRefresh = React.useRef(false);\n const mutationQueue = React.useRef(Promise.resolve());\n const pendingMutations = React.useRef(0);\n const optimisticLines = React.useRef(new Map<string, CartLinePayload>());\n\n const enqueueMutation = async (\n save: (currentCart: Cart) => Promise<Cart | null>,\n ) => {\n pendingMutations.current += 1;\n const request = mutationQueue.current.then(async () => {\n // An existing server cart remains usable if background initialization fails.\n await cartCreationPromise?.catch(() => null);\n if (needsRefresh.current) {\n const restoredCart = await getCartAction();\n if (!restoredCart)\n throw new CartContextError({\n message:\n \"Your cart could not be refreshed. Please reload before changing it.\",\n });\n serverCart.current = restoredCart;\n needsRefresh.current = false;\n }\n if (!serverCart.current) {\n throw new CartContextError({\n message: \"Your cart could not be loaded. Please try again.\",\n });\n }\n try {\n const savedCart = await save(serverCart.current);\n if (!savedCart)\n throw new CartContextError({\n message: \"Your cart could not be saved. Please try again.\",\n });\n serverCart.current = savedCart;\n } catch (error) {\n // A lost response may still have persisted the add that the next removal targets.\n const restoredCart = await getCartAction().catch(() => null);\n needsRefresh.current = !restoredCart;\n if (restoredCart) serverCart.current = restoredCart;\n throw error;\n }\n });\n // Cookie writes must finish in shopper order, including after a failed request.\n mutationQueue.current = request\n .catch(() => {})\n .then(() => {\n pendingMutations.current -= 1;\n if (pendingMutations.current === 0) {\n reportedLineQuantities.current.clear();\n optimisticLines.current.clear();\n updateCartData(serverCart.current);\n }\n });\n await request;\n };\n\n const findLine = ({\n currentCart,\n lineId,\n originalLine,\n }: {\n currentCart: Cart;\n lineId: string;\n originalLine?: CartLine;\n }) => {\n const existingLine = currentCart.lines.find((line) => line.id === lineId);\n if (existingLine) return existingLine;\n const optimisticLine = optimisticLines.current.get(lineId);\n const merchandiseId = optimisticLine?.id ?? originalLine?.merchandise?.id;\n const sellingPlanId =\n optimisticLine?.sellingPlanId ??\n originalLine?.sellingPlanAllocation?.sellingPlan.id ??\n null;\n return currentCart.lines.find((line) => {\n return (\n line.merchandise?.id === merchandiseId &&\n (line.sellingPlanAllocation?.sellingPlan.id ?? null) === sellingPlanId\n );\n });\n };\n\n const initializeCart = React.useCallback(async () => {\n const loadedCart = await getOrCreateSharedCart();\n if (!isMounted.current) {\n throw new DOMException(\"Cart provider was unmounted\", \"AbortError\");\n }\n serverCart.current = loadedCart;\n if (pendingMutations.current === 0) updateCartData(loadedCart);\n return loadedCart;\n }, [updateCartData]);\n\n // eslint-disable-next-line replo/no-use-effect -- Initializes the browser cart on mount.\n React.useEffect(() => {\n isMounted.current = true;\n void initializeCart().catch((error) => {\n if (isMounted.current) {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n }\n });\n return () => {\n isMounted.current = false;\n };\n }, [initializeCart]);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const emitCartView = React.useCallback(\n (cart: Cart | null) => {\n if (analytics) {\n void analytics.viewCart({ data: buildCartViewAnalyticsData(cart) });\n }\n },\n [analytics],\n );\n\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n emitCartView(cartData);\n }, [cartData, emitCartView]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Last quantity reported to analytics per line with an update in flight, so\n // overlapping stepper clicks diff against it instead of the same render-time\n // quantity. Dropped once the server confirms or another mutation replaces\n // the line set.\n const reportedLineQuantities = React.useRef(new Map<string, number>());\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // React callbacks retain their render's cart, even after initialization resolves.\n const readyCart =\n cartData ?? serverCart.current ?? (await initializeCart());\n\n reportedLineQuantities.current.clear();\n const additions = lines.map((line) => {\n const lineId = `temp-${uuidv4()}`;\n optimisticLines.current.set(lineId, line);\n return { line, lineId };\n });\n const applyLines = (cart: Cart) => {\n let updatedCart = { ...cart };\n for (const { line, lineId } of additions) {\n updatedCart = addLineToCart({ cart: updatedCart, line, lineId });\n }\n return updatedCart;\n };\n\n const updatedCart = updateCartData((currentCart) => {\n return applyLines(currentCart ?? readyCart);\n });\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n void analytics.addToCart({\n data: buildCartLineAnalyticsData({\n merchandise: line.merchandise,\n quantity: line.quantity,\n currencyCode: readyCart.cost.currencyCode,\n }),\n });\n }\n }\n\n if (openCartAfterAdd) {\n setIsCartOpen(true);\n emitCartView(updatedCart);\n }\n\n await enqueueMutation((currentCart) => {\n return addToCartAction(lines, currentCart.id);\n });\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Ad platforms have no \"quantity changed\" event: an increase is an add of\n // the difference and a decrease is a remove of the difference, matching\n // how Shopify themes report their cart steppers.\n const currentLine = cartData.lines.find((item) => item.id === line.id);\n const merchandise = currentLine?.merchandise ?? line.merchandise;\n const quantityDelta =\n line.quantity -\n (reportedLineQuantities.current.get(line.id) ??\n currentLine?.quantity ??\n 0);\n reportedLineQuantities.current.set(line.id, line.quantity);\n if (analytics && merchandise && quantityDelta !== 0) {\n const data = buildCartLineAnalyticsData({\n merchandise,\n quantity: Math.abs(quantityDelta),\n currencyCode: cartData.cost.currencyCode,\n });\n void (quantityDelta > 0\n ? analytics.addToCart({ data })\n : analytics.removeFromCart({ data }));\n }\n\n // Optimistic update - use functional form to avoid stale state\n updateCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n await enqueueMutation(async (currentCart) => {\n const savedLine = findLine({\n currentCart,\n lineId: line.id,\n originalLine: currentLine,\n });\n return savedLine\n ? await updateCartLineItemAction(\n { ...line, id: savedLine.id },\n currentCart.id,\n )\n : currentCart;\n });\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n reportedLineQuantities.current.delete(lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n void analytics.removeFromCart({\n data: buildCartLineAnalyticsData({\n merchandise: itemToRemove.merchandise,\n quantity: itemToRemove.quantity,\n currencyCode: cartData.cost.currencyCode,\n }),\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n updateCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n await enqueueMutation(async (currentCart) => {\n const savedLine = findLine({\n currentCart,\n lineId,\n originalLine: itemToRemove,\n });\n return savedLine\n ? await removeCartLineItemsAction([savedLine.id], currentCart.id)\n : currentCart;\n });\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n const readyCart =\n cartData ?? serverCart.current ?? (await initializeCart());\n\n updateCartData((currentCart) => {\n return {\n ...(currentCart ?? readyCart),\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n await enqueueMutation((currentCart) => {\n return updateCartDiscountCodesAction(discountCodes, currentCart.id);\n });\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": ["cart", "updatedCart"]
|
|
4
|
+
"sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type {\n Cart,\n CartLine,\n CartLineAdjustment,\n CartLinePayload,\n} from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport {\n addToCartAction,\n createCartAction,\n getCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport {\n deriveAddToCartAdjustments,\n deriveUpdateCartAdjustments,\n} from \"./utils/cart-adjustments\";\nimport {\n buildCartLineAnalyticsData,\n buildCartViewAnalyticsData,\n} from \"./utils/cart-analytics\";\nimport { CartContextError, toCartError } from \"./utils/cart-error\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nexport type CartOperation = \"add\" | \"update\" | \"remove\" | \"discount\";\n\n/**\n * The most recent cart change that could not be saved. `message` is written\n * for the shopper, so a cart UI can render it as-is next to a retry.\n */\nexport interface CartError {\n /** Which cart change failed */\n operation: CartOperation;\n /** Shopper-facing explanation with a retry prompt */\n message: string;\n}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart> | null = null;\n\nfunction getOrCreateSharedCart() {\n cartCreationPromise ??= getOrCreateCartAction()\n .then((cart) => {\n if (!cart) {\n throw new CartContextError({\n message: \"Your cart could not be loaded. Please try again.\",\n });\n }\n return cart;\n })\n .finally(() => {\n cartCreationPromise = null;\n });\n return cartCreationPromise;\n}\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart({\n cart,\n line,\n lineId,\n}: {\n cart: Cart;\n line: CartLinePayload;\n lineId: string;\n}): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: lineId,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<{ adjustments: CartLineAdjustment[] }>;\n /** Function to update cart item quantity */\n updateCartItem: (\n line: CartLinePayload,\n ) => Promise<{ adjustments: CartLineAdjustment[] }>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Re-read the persisted cart and adopt it if no mutations are in flight */\n refreshCart: () => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** The last cart change that failed to save; cleared when the next change starts */\n error: CartError | null;\n /** Function to dismiss the current error */\n clearError: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /**\n * Re-read the persisted cart and adopt it if no mutations are in flight.\n * Call after something outside the provider rewrote the cart \u2014 e.g. the\n * checkout route clamped it and bounced back with `?checkout=adjusted`.\n */\n refreshCart: () => Promise<void>;\n /**\n * The last cart change that could not be saved, with a shopper-ready\n * message. Set when an add, quantity change, removal, or discount fails;\n * cleared when the next change starts or via `clearError`. Render it in the\n * cart UI so the shopper knows to retry.\n */\n error: CartError | null;\n /** Function to dismiss the current error */\n clearError: () => void;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const optimisticCart = React.useRef(cart);\n const updateCartData = React.useCallback(\n (update: React.SetStateAction<Cart | null>) => {\n const nextCart =\n typeof update === \"function\" ? update(optimisticCart.current) : update;\n // Analytics and overlapping mutations need the snapshot before React renders it.\n optimisticCart.current = nextCart;\n setCartData(nextCart);\n return nextCart;\n },\n [],\n );\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const [error, setError] = React.useState<CartError | null>(null);\n const clearError = React.useCallback(() => setError(null), []);\n const isMounted = React.useRef(true);\n\n const runWithErrorState = async <ResultType,>(\n operation: CartOperation,\n mutate: () => Promise<ResultType>,\n ) => {\n setError(null);\n try {\n return await mutate();\n } catch (caught) {\n console.error(`[Replo] Cart ${operation} failed:`, caught);\n setError(toCartError(operation, caught));\n throw caught;\n }\n };\n const analytics = useAnalyticsOptional();\n const serverCart = React.useRef(cart);\n const needsRefresh = React.useRef(false);\n const mutationQueue = React.useRef(Promise.resolve());\n const pendingMutations = React.useRef(0);\n const optimisticLines = React.useRef(new Map<string, CartLinePayload>());\n\n const enqueueMutation = async (\n save: (currentCart: Cart) => Promise<Cart | null>,\n ) => {\n pendingMutations.current += 1;\n const request = mutationQueue.current.then(async () => {\n // An existing server cart remains usable if background initialization fails.\n await cartCreationPromise?.catch(() => null);\n if (needsRefresh.current) {\n const restoredCart = await getCartAction();\n if (!restoredCart)\n throw new CartContextError({\n message:\n \"Your cart could not be refreshed. Please reload before changing it.\",\n });\n serverCart.current = restoredCart;\n needsRefresh.current = false;\n }\n if (!serverCart.current) {\n throw new CartContextError({\n message: \"Your cart could not be loaded. Please try again.\",\n });\n }\n try {\n const savedCart = await save(serverCart.current);\n if (!savedCart)\n throw new CartContextError({\n message: \"Your cart could not be saved. Please try again.\",\n });\n serverCart.current = savedCart;\n } catch (error) {\n // A lost response may still have persisted the add that the next removal targets.\n const restoredCart = await getCartAction().catch(() => null);\n needsRefresh.current = !restoredCart;\n if (restoredCart) serverCart.current = restoredCart;\n throw error;\n }\n });\n // Cookie writes must finish in shopper order, including after a failed request.\n mutationQueue.current = request\n .catch(() => {})\n .then(() => {\n pendingMutations.current -= 1;\n if (pendingMutations.current === 0) {\n reportedLineQuantities.current.clear();\n optimisticLines.current.clear();\n updateCartData(serverCart.current);\n }\n });\n await request;\n };\n\n const findLine = ({\n currentCart,\n lineId,\n originalLine,\n }: {\n currentCart: Cart;\n lineId: string;\n originalLine?: CartLine;\n }) => {\n const existingLine = currentCart.lines.find((line) => line.id === lineId);\n if (existingLine) return existingLine;\n const optimisticLine = optimisticLines.current.get(lineId);\n const merchandiseId = optimisticLine?.id ?? originalLine?.merchandise?.id;\n const sellingPlanId =\n optimisticLine?.sellingPlanId ??\n originalLine?.sellingPlanAllocation?.sellingPlan.id ??\n null;\n return currentCart.lines.find((line) => {\n return (\n line.merchandise?.id === merchandiseId &&\n (line.sellingPlanAllocation?.sellingPlan.id ?? null) === sellingPlanId\n );\n });\n };\n\n const initializeCart = React.useCallback(async () => {\n const loadedCart = await getOrCreateSharedCart();\n if (!isMounted.current) {\n throw new DOMException(\"Cart provider was unmounted\", \"AbortError\");\n }\n serverCart.current = loadedCart;\n if (pendingMutations.current === 0) updateCartData(loadedCart);\n return loadedCart;\n }, [updateCartData]);\n\n // eslint-disable-next-line replo/no-use-effect -- Initializes the browser cart on mount.\n React.useEffect(() => {\n isMounted.current = true;\n void initializeCart().catch((error) => {\n if (isMounted.current) {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n }\n });\n return () => {\n isMounted.current = false;\n };\n }, [initializeCart]);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const emitCartView = React.useCallback(\n (cart: Cart | null) => {\n if (analytics) {\n void analytics.viewCart({ data: buildCartViewAnalyticsData(cart) });\n }\n },\n [analytics],\n );\n\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n emitCartView(cartData);\n }, [cartData, emitCartView]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n const refreshCart = React.useCallback(async () => {\n const refreshedCart = await getCartAction();\n // In-flight mutations own serverCart; a slow refresh must not clobber\n // their newer result with this older snapshot.\n if (\n !isMounted.current ||\n !refreshedCart ||\n pendingMutations.current !== 0\n ) {\n return;\n }\n serverCart.current = refreshedCart;\n updateCartData(refreshedCart);\n }, [updateCartData]);\n\n // Last quantity reported to analytics per line with an update in flight, so\n // overlapping stepper clicks diff against it instead of the same render-time\n // quantity. Dropped once the server confirms or another mutation replaces\n // the line set.\n const reportedLineQuantities = React.useRef(new Map<string, number>());\n\n const addToCart = (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n return runWithErrorState(\"add\", async () => {\n // React callbacks retain their render's cart, even after initialization resolves.\n const readyCart =\n cartData ??\n serverCart.current ??\n (await initializeCart().catch((caught: unknown) => {\n // The cart UI is where the failure is shown, so a first add that fails\n // before it would have opened the cart still opens it.\n if (openCartAfterAdd) setIsCartOpen(true);\n throw caught;\n }));\n\n reportedLineQuantities.current.clear();\n const additions = lines.map((line) => {\n const lineId = `temp-${uuidv4()}`;\n optimisticLines.current.set(lineId, line);\n return { line, lineId };\n });\n const applyLines = (cart: Cart) => {\n let updatedCart = { ...cart };\n for (const { line, lineId } of additions) {\n updatedCart = addLineToCart({ cart: updatedCart, line, lineId });\n }\n return updatedCart;\n };\n\n const updatedCart = updateCartData((currentCart) => {\n return applyLines(currentCart ?? readyCart);\n });\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n void analytics.addToCart({\n data: buildCartLineAnalyticsData({\n merchandise: line.merchandise,\n quantity: line.quantity,\n currencyCode: readyCart.cost.currencyCode,\n }),\n });\n }\n }\n\n if (openCartAfterAdd) {\n setIsCartOpen(true);\n emitCartView(updatedCart);\n }\n\n // The write path reconciles the whole cart, so any line can be clamped\n // or dropped \u2014 not just the ones this call added. Intent comes from the\n // request itself, not the optimistic cart, whose lines can lack\n // merchandise or plan metadata.\n const adjustments: CartLineAdjustment[] = [];\n await enqueueMutation(async (currentCart) => {\n const savedCart = await addToCartAction(lines, currentCart.id);\n if (savedCart) {\n adjustments.push(\n ...deriveAddToCartAdjustments({\n cartBeforeAdd: currentCart,\n requestedLines: lines,\n savedCart,\n }),\n );\n }\n return savedCart;\n });\n return { adjustments };\n });\n };\n\n const updateCartItem = (line: CartLinePayload) => {\n return runWithErrorState(\"update\", async () => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return { adjustments: [] };\n }\n\n // Ad platforms have no \"quantity changed\" event: an increase is an add of\n // the difference and a decrease is a remove of the difference, matching\n // how Shopify themes report their cart steppers.\n const currentLine = cartData.lines.find((item) => item.id === line.id);\n const merchandise = currentLine?.merchandise ?? line.merchandise;\n const quantityDelta =\n line.quantity -\n (reportedLineQuantities.current.get(line.id) ??\n currentLine?.quantity ??\n 0);\n reportedLineQuantities.current.set(line.id, line.quantity);\n if (analytics && merchandise && quantityDelta !== 0) {\n const data = buildCartLineAnalyticsData({\n merchandise,\n quantity: Math.abs(quantityDelta),\n currencyCode: cartData.cost.currencyCode,\n });\n void (quantityDelta > 0\n ? analytics.addToCart({ data })\n : analytics.removeFromCart({ data }));\n }\n\n // Optimistic update - use functional form to avoid stale state\n updateCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n const adjustments: CartLineAdjustment[] = [];\n await enqueueMutation(async (currentCart) => {\n const savedLine = findLine({\n currentCart,\n lineId: line.id,\n originalLine: currentLine,\n });\n if (!savedLine) {\n return currentCart;\n }\n const savedCart = await updateCartLineItemAction(\n { ...line, id: savedLine.id },\n currentCart.id,\n );\n if (savedCart) {\n adjustments.push(\n ...deriveUpdateCartAdjustments({\n cartBeforeUpdate: currentCart,\n editedLine: savedLine,\n requestedQuantity: line.quantity,\n requestedSellingPlanId: line.sellingPlanId,\n savedCart,\n }),\n );\n }\n return savedCart;\n });\n return { adjustments };\n });\n };\n\n const removeCartItem = (lineId: string) => {\n return runWithErrorState(\"remove\", async () => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n reportedLineQuantities.current.delete(lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n void analytics.removeFromCart({\n data: buildCartLineAnalyticsData({\n merchandise: itemToRemove.merchandise,\n quantity: itemToRemove.quantity,\n currencyCode: cartData.cost.currencyCode,\n }),\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n updateCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n await enqueueMutation(async (currentCart) => {\n const savedLine = findLine({\n currentCart,\n lineId,\n originalLine: itemToRemove,\n });\n return savedLine\n ? await removeCartLineItemsAction([savedLine.id], currentCart.id)\n : currentCart;\n });\n });\n };\n\n const updateDiscountCodes = (discountCodes: string[]) => {\n return runWithErrorState(\"discount\", async () => {\n const readyCart =\n cartData ?? serverCart.current ?? (await initializeCart());\n\n updateCartData((currentCart) => {\n return {\n ...(currentCart ?? readyCart),\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n await enqueueMutation((currentCart) => {\n return updateCartDiscountCodesAction(discountCodes, currentCart.id);\n });\n });\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n refreshCart,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n error,\n clearError,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n refreshCart: context.refreshCart,\n error: context.error,\n clearError: context.clearError,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["error", "cart", "updatedCart"]
|
|
7
7
|
}
|
package/cart/cart-types.d.ts
CHANGED
|
@@ -119,6 +119,21 @@ export interface Cart {
|
|
|
119
119
|
discountCodes?: CartDiscountCode[];
|
|
120
120
|
discountAllocations?: CartDiscountAllocation[];
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* How a cart write differed from what the shopper asked for, derived by
|
|
124
|
+
* comparing the requested lines against the cart the server saved. Cart UIs
|
|
125
|
+
* render these as Shopify-style notices ("Only 9 in stock — we've updated
|
|
126
|
+
* your cart"); the underlying clamp has already been applied to the cart.
|
|
127
|
+
*/
|
|
128
|
+
export type CartLineAdjustment = {
|
|
129
|
+
type: "quantityClampedToStock";
|
|
130
|
+
variantId: string;
|
|
131
|
+
requested: number;
|
|
132
|
+
applied: number;
|
|
133
|
+
} | {
|
|
134
|
+
type: "unavailableLineRemoved";
|
|
135
|
+
variantId: string;
|
|
136
|
+
};
|
|
122
137
|
export interface CartLinePayload {
|
|
123
138
|
id: string;
|
|
124
139
|
quantity: number;
|
package/cart/cart-types.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/cart-types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * TypeScript types for cart data structures. Use these types when working with cart data, but don't import from this module directly in components - use the cart hooks instead.\n * @module\n */\n\nimport type { SellingPlanGroup } from \"../loaders/loader-schemas\";\n\nexport interface Image {\n url: string;\n altText: string | null;\n}\n\nexport interface CartLineMerchandise {\n id: string;\n product: {\n id: string;\n title: string;\n sellingPlanGroups?: SellingPlanGroup[];\n };\n title: string;\n /** Integer minor units (Stripe convention): 1999 = $19.99 USD */\n price: number;\n compareAtPrice?: number | null;\n image: Image | null;\n selectedOptions: {\n name: string;\n value: string;\n }[];\n}\n\nexport interface CartLineAttribute {\n key: string;\n value: string | number | boolean;\n}\n\n/** Replo attribution property added to cart/buy-now line items for order attribution in Shopify. */\nexport const REPLO_ATTRIBUTION_PROPERTY: CartLineAttribute = {\n key: \"_replo\",\n value: \"true\",\n};\n\nexport interface SellingPlanPriceAdjustment {\n compareAtPrice: {\n amount: number;\n currencyCode: string;\n };\n perDeliveryPrice: {\n amount: number;\n currencyCode: string;\n };\n price: {\n amount: number;\n currencyCode: string;\n };\n unitPrice: {\n amount: number;\n currencyCode: string;\n } | null;\n}\n\nexport interface SellingPlanAllocation {\n sellingPlan: {\n id: string;\n name: string;\n description: string | null;\n };\n priceAdjustments: SellingPlanPriceAdjustment[];\n checkoutChargeAmount: {\n amount: number;\n currencyCode: string;\n };\n remainingBalanceChargeAmount: {\n amount: number;\n currencyCode: string;\n };\n}\n\nexport interface CartLine {\n id: string;\n quantity: number;\n /**\n * Null when this is an optimistic line whose merchandise couldn't be\n * resolved from the query cache or passed explicitly. Consumers should\n * render a skeleton/loading state in that case; the line is replaced\n * with real merchandise once the server roundtrip lands.\n */\n merchandise: CartLineMerchandise | null;\n attributes: CartLineAttribute[];\n sellingPlanAllocation?: SellingPlanAllocation | null;\n}\n\nexport interface CartCost {\n /** Integer minor units (Stripe convention) in currencyCode */\n subtotalAmount: number;\n totalAmount: number;\n currencyCode: string;\n}\n\n/**\n * Represents a discount code applied to a cart.\n */\nexport interface CartDiscountCode {\n /** The discount code string */\n code: string;\n /**\n * Whether the discount code is applicable to the current cart.\n * A code may not be applicable due to:\n * - Cart contents not meeting product/collection eligibility\n * - Minimum purchase amount not met\n * - Code expired or usage limits reached\n * - Customer restrictions\n * - Conflicting discount rules\n */\n applicable: boolean;\n}\n\n/**\n * One discount Shopify applied to a cart line or to the whole cart. The amount\n * is already subtracted from `cost.totalAmount` (and from `cost.subtotalAmount`\n * for line-level discounts).\n */\nexport interface CartDiscountAllocation {\n /** Integer minor units (Stripe convention) in the cart's currencyCode */\n discountedAmount: number;\n /** Automatic discount title or discount code; null when Shopify has neither */\n title: string | null;\n}\n\nexport interface Cart {\n id: string;\n lines: CartLine[];\n cost: CartCost;\n checkoutUrl?: string;\n discountCodes?: CartDiscountCode[];\n discountAllocations?: CartDiscountAllocation[];\n}\n\nexport interface CartLinePayload {\n id: string;\n quantity: number;\n merchandise?: CartLineMerchandise;\n properties?: CartLineAttribute[];\n sellingPlanId?: string | null;\n}\n\nexport interface AddToCartPayload {\n lines: CartLinePayload[];\n cartId: string;\n}\n\nexport interface UpdateCartLineItemPayload {\n line: CartLinePayload;\n cartId: string;\n}\n\nexport interface RemoveFromCartPayload {\n lineIds: string[];\n cartId: string;\n}\n\nexport interface UpdateCartDiscountCodesPayload {\n cartId: string;\n discountCodes: string[];\n}\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * TypeScript types for cart data structures. Use these types when working with cart data, but don't import from this module directly in components - use the cart hooks instead.\n * @module\n */\n\nimport type { SellingPlanGroup } from \"../loaders/loader-schemas\";\n\nexport interface Image {\n url: string;\n altText: string | null;\n}\n\nexport interface CartLineMerchandise {\n id: string;\n product: {\n id: string;\n title: string;\n sellingPlanGroups?: SellingPlanGroup[];\n };\n title: string;\n /** Integer minor units (Stripe convention): 1999 = $19.99 USD */\n price: number;\n compareAtPrice?: number | null;\n image: Image | null;\n selectedOptions: {\n name: string;\n value: string;\n }[];\n}\n\nexport interface CartLineAttribute {\n key: string;\n value: string | number | boolean;\n}\n\n/** Replo attribution property added to cart/buy-now line items for order attribution in Shopify. */\nexport const REPLO_ATTRIBUTION_PROPERTY: CartLineAttribute = {\n key: \"_replo\",\n value: \"true\",\n};\n\nexport interface SellingPlanPriceAdjustment {\n compareAtPrice: {\n amount: number;\n currencyCode: string;\n };\n perDeliveryPrice: {\n amount: number;\n currencyCode: string;\n };\n price: {\n amount: number;\n currencyCode: string;\n };\n unitPrice: {\n amount: number;\n currencyCode: string;\n } | null;\n}\n\nexport interface SellingPlanAllocation {\n sellingPlan: {\n id: string;\n name: string;\n description: string | null;\n };\n priceAdjustments: SellingPlanPriceAdjustment[];\n checkoutChargeAmount: {\n amount: number;\n currencyCode: string;\n };\n remainingBalanceChargeAmount: {\n amount: number;\n currencyCode: string;\n };\n}\n\nexport interface CartLine {\n id: string;\n quantity: number;\n /**\n * Null when this is an optimistic line whose merchandise couldn't be\n * resolved from the query cache or passed explicitly. Consumers should\n * render a skeleton/loading state in that case; the line is replaced\n * with real merchandise once the server roundtrip lands.\n */\n merchandise: CartLineMerchandise | null;\n attributes: CartLineAttribute[];\n sellingPlanAllocation?: SellingPlanAllocation | null;\n}\n\nexport interface CartCost {\n /** Integer minor units (Stripe convention) in currencyCode */\n subtotalAmount: number;\n totalAmount: number;\n currencyCode: string;\n}\n\n/**\n * Represents a discount code applied to a cart.\n */\nexport interface CartDiscountCode {\n /** The discount code string */\n code: string;\n /**\n * Whether the discount code is applicable to the current cart.\n * A code may not be applicable due to:\n * - Cart contents not meeting product/collection eligibility\n * - Minimum purchase amount not met\n * - Code expired or usage limits reached\n * - Customer restrictions\n * - Conflicting discount rules\n */\n applicable: boolean;\n}\n\n/**\n * One discount Shopify applied to a cart line or to the whole cart. The amount\n * is already subtracted from `cost.totalAmount` (and from `cost.subtotalAmount`\n * for line-level discounts).\n */\nexport interface CartDiscountAllocation {\n /** Integer minor units (Stripe convention) in the cart's currencyCode */\n discountedAmount: number;\n /** Automatic discount title or discount code; null when Shopify has neither */\n title: string | null;\n}\n\nexport interface Cart {\n id: string;\n lines: CartLine[];\n cost: CartCost;\n checkoutUrl?: string;\n discountCodes?: CartDiscountCode[];\n discountAllocations?: CartDiscountAllocation[];\n}\n\n/**\n * How a cart write differed from what the shopper asked for, derived by\n * comparing the requested lines against the cart the server saved. Cart UIs\n * render these as Shopify-style notices (\"Only 9 in stock \u2014 we've updated\n * your cart\"); the underlying clamp has already been applied to the cart.\n */\nexport type CartLineAdjustment =\n | {\n type: \"quantityClampedToStock\";\n variantId: string;\n requested: number;\n applied: number;\n }\n | { type: \"unavailableLineRemoved\"; variantId: string };\n\nexport interface CartLinePayload {\n id: string;\n quantity: number;\n merchandise?: CartLineMerchandise;\n properties?: CartLineAttribute[];\n sellingPlanId?: string | null;\n}\n\nexport interface AddToCartPayload {\n lines: CartLinePayload[];\n cartId: string;\n}\n\nexport interface UpdateCartLineItemPayload {\n line: CartLinePayload;\n cartId: string;\n}\n\nexport interface RemoveFromCartPayload {\n lineIds: string[];\n cartId: string;\n}\n\nexport interface UpdateCartDiscountCodesPayload {\n cartId: string;\n discountCodes: string[];\n}\n"],
|
|
5
5
|
"mappings": "AAoCO,MAAM,6BAAgD;AAAA,EAC3D,KAAK;AAAA,EACL,OAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { AddToCartPayload, Cart, CartLinePayload, RemoveFromCartPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload } from "../cart-types";
|
|
2
2
|
export interface CartGateway {
|
|
3
3
|
getCart(cartId: string): Promise<Cart | null>;
|
|
4
|
+
/**
|
|
5
|
+
* Bring a stored cart back in line with its source of truth. Cookie carts
|
|
6
|
+
* reprice against the catalog and drop unavailable lines (persisting the
|
|
7
|
+
* result); provider-backed carts are already server-truth and return the
|
|
8
|
+
* cart unchanged.
|
|
9
|
+
*/
|
|
10
|
+
reconcileStoredCart(cart: Cart): Promise<Cart>;
|
|
4
11
|
createCart(lines?: CartLinePayload[], discountCodes?: string[]): Promise<Cart | null>;
|
|
5
12
|
addToCart(cartPayload: AddToCartPayload): Promise<Cart | null>;
|
|
6
13
|
updateCartLineItem(cartPayload: UpdateCartLineItemPayload): Promise<Cart | null>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/gateways/cart-gateway.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\n\nimport { getCheckoutProvider } from \"../../lib/integration-utils\";\nimport { createDefaultCartGateway } from \"./default-cart-gateway\";\nimport { createResolverCartGateway } from \"./resolver-cart-gateway\";\nimport { createStripeCartGateway } from \"./stripe-cart-gateway\";\n\nexport interface CartGateway {\n getCart(cartId: string): Promise<Cart | null>;\n createCart(\n lines?: CartLinePayload[],\n discountCodes?: string[],\n ): Promise<Cart | null>;\n addToCart(cartPayload: AddToCartPayload): Promise<Cart | null>;\n updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null>;\n removeCartLineItems(cartPayload: RemoveFromCartPayload): Promise<Cart | null>;\n updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null>;\n}\n\nexport async function getCartGateway(): Promise<CartGateway> {\n const checkoutProvider = await getCheckoutProvider();\n\n if (checkoutProvider === \"stripe\") {\n return await createStripeCartGateway();\n } else if (checkoutProvider === \"shopify\") {\n return await createResolverCartGateway();\n } else {\n return await createDefaultCartGateway();\n }\n}\n"],
|
|
5
|
-
"mappings": ";AAWA,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AACzC,SAAS,iCAAiC;AAC1C,SAAS,+BAA+B;
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\n\nimport { getCheckoutProvider } from \"../../lib/integration-utils\";\nimport { createDefaultCartGateway } from \"./default-cart-gateway\";\nimport { createResolverCartGateway } from \"./resolver-cart-gateway\";\nimport { createStripeCartGateway } from \"./stripe-cart-gateway\";\n\nexport interface CartGateway {\n getCart(cartId: string): Promise<Cart | null>;\n /**\n * Bring a stored cart back in line with its source of truth. Cookie carts\n * reprice against the catalog and drop unavailable lines (persisting the\n * result); provider-backed carts are already server-truth and return the\n * cart unchanged.\n */\n reconcileStoredCart(cart: Cart): Promise<Cart>;\n createCart(\n lines?: CartLinePayload[],\n discountCodes?: string[],\n ): Promise<Cart | null>;\n addToCart(cartPayload: AddToCartPayload): Promise<Cart | null>;\n updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null>;\n removeCartLineItems(cartPayload: RemoveFromCartPayload): Promise<Cart | null>;\n updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null>;\n}\n\nexport async function getCartGateway(): Promise<CartGateway> {\n const checkoutProvider = await getCheckoutProvider();\n\n if (checkoutProvider === \"stripe\") {\n return await createStripeCartGateway();\n } else if (checkoutProvider === \"shopify\") {\n return await createResolverCartGateway();\n } else {\n return await createDefaultCartGateway();\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAWA,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AACzC,SAAS,iCAAiC;AAC1C,SAAS,+BAA+B;AAyBxC,eAAsB,iBAAuC;AAC3D,QAAM,mBAAmB,MAAM,oBAAoB;AAEnD,MAAI,qBAAqB,UAAU;AACjC,WAAO,MAAM,wBAAwB;AAAA,EACvC,WAAW,qBAAqB,WAAW;AACzC,WAAO,MAAM,0BAA0B;AAAA,EACzC,OAAO;AACL,WAAO,MAAM,yBAAyB;AAAA,EACxC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReconciledCartLine } from "../../gateways/canopy-api";
|
|
1
2
|
import type { AddToCartPayload, Cart, CartLinePayload, RemoveFromCartPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload } from "../cart-types";
|
|
2
3
|
import type { LineMergeFn } from "../utils/cookie-cart-persistence";
|
|
3
4
|
import type { CartGateway } from "./cart-gateway";
|
|
@@ -10,6 +11,10 @@ interface CookieCartConfig {
|
|
|
10
11
|
lineIdGenerator: () => string;
|
|
11
12
|
cartIdGenerator: () => string;
|
|
12
13
|
gatewayName: string;
|
|
14
|
+
reconcileLines?: (lines: {
|
|
15
|
+
variantId: string;
|
|
16
|
+
quantity: number;
|
|
17
|
+
}[]) => Promise<ReconciledCartLine[]>;
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
20
|
* Abstract base class for cookie-based cart gateways.
|
|
@@ -18,6 +23,10 @@ interface CookieCartConfig {
|
|
|
18
23
|
*/
|
|
19
24
|
export declare abstract class CookieBasedCartGateway implements CartGateway {
|
|
20
25
|
protected abstract readonly config: CookieCartConfig;
|
|
26
|
+
private fetchReconciledLines;
|
|
27
|
+
private reconcile;
|
|
28
|
+
private reconcileForWrite;
|
|
29
|
+
reconcileStoredCart(cart: Cart): Promise<Cart>;
|
|
21
30
|
getCart(cartId: string): Promise<Cart | null>;
|
|
22
31
|
createCart(lines?: CartLinePayload[], _discountCodes?: string[]): Promise<Cart | null>;
|
|
23
32
|
addToCart(cartPayload: AddToCartPayload): Promise<Cart | null>;
|
|
@@ -1,11 +1,59 @@
|
|
|
1
|
+
import { applyCartLineReconciliation } from "../utils/cart-utils";
|
|
1
2
|
import {
|
|
2
3
|
addToCartWithPersistence,
|
|
3
4
|
createCartWithPersistence,
|
|
4
5
|
loadCartFromCookie,
|
|
5
6
|
removeCartLineItemsWithPersistence,
|
|
7
|
+
saveCartToCookie,
|
|
6
8
|
updateCartLineItemWithPersistence
|
|
7
9
|
} from "../utils/cookie-cart-persistence";
|
|
8
10
|
class CookieBasedCartGateway {
|
|
11
|
+
// Fails open (null) when the catalog can't answer: a canopy blip must not
|
|
12
|
+
// block cart writes — checkout still enforces stock and price.
|
|
13
|
+
async fetchReconciledLines(cart) {
|
|
14
|
+
const reconcileLines = this.config.reconcileLines;
|
|
15
|
+
if (!reconcileLines) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const requestLines = cart.lines.flatMap((line) => {
|
|
19
|
+
return line.merchandise ? [{ variantId: line.merchandise.id, quantity: line.quantity }] : [];
|
|
20
|
+
});
|
|
21
|
+
if (requestLines.length === 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return await reconcileLines(requestLines);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn(
|
|
28
|
+
`${this.config.gatewayName} cart reconciliation unavailable, keeping cart as written:`,
|
|
29
|
+
error
|
|
30
|
+
);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Catalog fetch failures fail open inside fetchReconciledLines; a
|
|
35
|
+
// reconciliation mismatch from the apply still throws.
|
|
36
|
+
async reconcile(cart, { clampQuantities }) {
|
|
37
|
+
const reconciledLines = await this.fetchReconciledLines(cart);
|
|
38
|
+
if (!reconciledLines) {
|
|
39
|
+
return cart;
|
|
40
|
+
}
|
|
41
|
+
return applyCartLineReconciliation(cart, reconciledLines, {
|
|
42
|
+
clampQuantities
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
reconcileForWrite = (cart) => {
|
|
46
|
+
return this.reconcile(cart, { clampQuantities: true });
|
|
47
|
+
};
|
|
48
|
+
async reconcileStoredCart(cart) {
|
|
49
|
+
const reconciledCart = await this.reconcile(cart, {
|
|
50
|
+
clampQuantities: false
|
|
51
|
+
});
|
|
52
|
+
if (JSON.stringify(reconciledCart.lines) !== JSON.stringify(cart.lines)) {
|
|
53
|
+
await saveCartToCookie(reconciledCart, this.config.cookieName);
|
|
54
|
+
}
|
|
55
|
+
return reconciledCart;
|
|
56
|
+
}
|
|
9
57
|
async getCart(cartId) {
|
|
10
58
|
try {
|
|
11
59
|
const cart = await loadCartFromCookie(this.config.cookieName);
|
|
@@ -24,7 +72,8 @@ class CookieBasedCartGateway {
|
|
|
24
72
|
cookieName: this.config.cookieName,
|
|
25
73
|
lines,
|
|
26
74
|
lineIdGenerator: this.config.lineIdGenerator,
|
|
27
|
-
cartIdGenerator: this.config.cartIdGenerator
|
|
75
|
+
cartIdGenerator: this.config.cartIdGenerator,
|
|
76
|
+
reconcile: this.reconcileForWrite
|
|
28
77
|
});
|
|
29
78
|
return cart;
|
|
30
79
|
} catch (createCartError) {
|
|
@@ -40,7 +89,8 @@ class CookieBasedCartGateway {
|
|
|
40
89
|
const cartData = await addToCartWithPersistence(cartPayload, {
|
|
41
90
|
cookieName: this.config.cookieName,
|
|
42
91
|
mergeFn: this.config.lineMergeFn,
|
|
43
|
-
lineIdGenerator: this.config.lineIdGenerator
|
|
92
|
+
lineIdGenerator: this.config.lineIdGenerator,
|
|
93
|
+
reconcile: this.reconcileForWrite
|
|
44
94
|
});
|
|
45
95
|
return cartData;
|
|
46
96
|
} catch (error) {
|
|
@@ -56,7 +106,8 @@ class CookieBasedCartGateway {
|
|
|
56
106
|
try {
|
|
57
107
|
const cartData = await updateCartLineItemWithPersistence(
|
|
58
108
|
cartPayload,
|
|
59
|
-
this.config.cookieName
|
|
109
|
+
this.config.cookieName,
|
|
110
|
+
this.reconcileForWrite
|
|
60
111
|
);
|
|
61
112
|
return cartData;
|
|
62
113
|
} catch (error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/gateways/cookie-cart-gateway.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\nimport type { LineMergeFn } from \"../utils/cookie-cart-persistence\";\nimport type { CartGateway } from \"./cart-gateway\";\n\nimport {\n addToCartWithPersistence,\n createCartWithPersistence,\n loadCartFromCookie,\n removeCartLineItemsWithPersistence,\n updateCartLineItemWithPersistence,\n} from \"../utils/cookie-cart-persistence\";\n\n/**\n * Configuration for cookie-based cart gateways\n */\ninterface CookieCartConfig {\n cookieName: string;\n lineMergeFn: LineMergeFn;\n lineIdGenerator: () => string;\n cartIdGenerator: () => string;\n gatewayName: string; // For logging (e.g., \"Stripe\", \"Default\")\n}\n\n/**\n * Abstract base class for cookie-based cart gateways.\n * Provides common error handling, logging, and cart operations.\n * Subclasses must provide gateway-specific configuration.\n */\nexport abstract class CookieBasedCartGateway implements CartGateway {\n protected abstract readonly config: CookieCartConfig;\n\n async getCart(cartId: string): Promise<Cart | null> {\n try {\n const cart = await loadCartFromCookie(this.config.cookieName);\n return cart?.id === cartId ? cart : null;\n } catch (error) {\n console.warn(\n `${this.config.gatewayName} cart ${cartId} not found, could not fetch cart:`,\n error,\n );\n return null;\n }\n }\n\n async createCart(\n lines?: CartLinePayload[],\n _discountCodes?: string[],\n ): Promise<Cart | null> {\n try {\n const cart = await createCartWithPersistence({\n cookieName: this.config.cookieName,\n lines,\n lineIdGenerator: this.config.lineIdGenerator,\n cartIdGenerator: this.config.cartIdGenerator,\n });\n return cart;\n } catch (createCartError) {\n console.warn(\n `Failed to create ${this.config.gatewayName} cart${lines?.length ? \" with lines\" : \"\"}:`,\n createCartError,\n );\n return null;\n }\n }\n\n async addToCart(cartPayload: AddToCartPayload): Promise<Cart | null> {\n try {\n const cartData = await addToCartWithPersistence(cartPayload, {\n cookieName: this.config.cookieName,\n mergeFn: this.config.lineMergeFn,\n lineIdGenerator: this.config.lineIdGenerator,\n });\n return cartData;\n } catch (error) {\n console.warn(`Failed to add items to ${this.config.gatewayName} cart:`, {\n error: error instanceof Error ? error.message : String(error),\n cartId: cartPayload.cartId,\n itemCount: cartPayload.lines.length,\n });\n return null;\n }\n }\n\n async updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null> {\n try {\n const cartData = await updateCartLineItemWithPersistence(\n cartPayload,\n this.config.cookieName,\n );\n return cartData;\n } catch (error) {\n console.warn(\n `Failed to update ${this.config.gatewayName} cart line item:`,\n {\n error: error instanceof Error ? error.message : String(error),\n cartId: cartPayload.cartId,\n lineId: cartPayload.line.id,\n quantity: cartPayload.line.quantity,\n },\n );\n return null;\n }\n }\n\n async removeCartLineItems(\n cartPayload: RemoveFromCartPayload,\n ): Promise<Cart | null> {\n try {\n const cartData = await removeCartLineItemsWithPersistence(\n cartPayload,\n this.config.cookieName,\n );\n return cartData;\n } catch (error) {\n console.warn(\n `Failed to remove ${this.config.gatewayName} cart line items:`,\n {\n error: error instanceof Error ? error.message : String(error),\n cartId: cartPayload.cartId,\n lineIds: cartPayload.lineIds,\n },\n );\n return null;\n }\n }\n\n async updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null> {\n console.warn(\n `${this.config.gatewayName} cart gateway does not yet support discount codes`,\n );\n // Return existing cart without changes\n try {\n return await this.getCart(cartPayload.cartId);\n } catch (error) {\n console.warn(\"Failed to fetch cart for discount code update:\", error);\n return null;\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { ReconciledCartLine } from \"../../gateways/canopy-api\";\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\nimport type { LineMergeFn } from \"../utils/cookie-cart-persistence\";\nimport type { CartGateway } from \"./cart-gateway\";\n\nimport { applyCartLineReconciliation } from \"../utils/cart-utils\";\nimport {\n addToCartWithPersistence,\n createCartWithPersistence,\n loadCartFromCookie,\n removeCartLineItemsWithPersistence,\n saveCartToCookie,\n updateCartLineItemWithPersistence,\n} from \"../utils/cookie-cart-persistence\";\n\n/**\n * Configuration for cookie-based cart gateways\n */\ninterface CookieCartConfig {\n cookieName: string;\n lineMergeFn: LineMergeFn;\n lineIdGenerator: () => string;\n cartIdGenerator: () => string;\n gatewayName: string; // For logging (e.g., \"Stripe\", \"Default\")\n // The catalog's per-line facts for this cart's variants. When present,\n // every cart write reconciles against it (reprice, clamp, drop\n // unavailable); absent means the gateway has no catalog to ask.\n reconcileLines?: (\n lines: { variantId: string; quantity: number }[],\n ) => Promise<ReconciledCartLine[]>;\n}\n\n/**\n * Abstract base class for cookie-based cart gateways.\n * Provides common error handling, logging, and cart operations.\n * Subclasses must provide gateway-specific configuration.\n */\nexport abstract class CookieBasedCartGateway implements CartGateway {\n protected abstract readonly config: CookieCartConfig;\n\n // Fails open (null) when the catalog can't answer: a canopy blip must not\n // block cart writes \u2014 checkout still enforces stock and price.\n private async fetchReconciledLines(\n cart: Cart,\n ): Promise<ReconciledCartLine[] | null> {\n const reconcileLines = this.config.reconcileLines;\n if (!reconcileLines) {\n return null;\n }\n const requestLines = cart.lines.flatMap((line) => {\n return line.merchandise\n ? [{ variantId: line.merchandise.id, quantity: line.quantity }]\n : [];\n });\n if (requestLines.length === 0) {\n return null;\n }\n try {\n return await reconcileLines(requestLines);\n } catch (error) {\n console.warn(\n `${this.config.gatewayName} cart reconciliation unavailable, keeping cart as written:`,\n error,\n );\n return null;\n }\n }\n\n // Catalog fetch failures fail open inside fetchReconciledLines; a\n // reconciliation mismatch from the apply still throws.\n private async reconcile(\n cart: Cart,\n { clampQuantities }: { clampQuantities: boolean },\n ): Promise<Cart> {\n const reconciledLines = await this.fetchReconciledLines(cart);\n if (!reconciledLines) {\n return cart;\n }\n return applyCartLineReconciliation(cart, reconciledLines, {\n clampQuantities,\n });\n }\n\n private reconcileForWrite = (cart: Cart): Promise<Cart> => {\n return this.reconcile(cart, { clampQuantities: true });\n };\n\n async reconcileStoredCart(cart: Cart): Promise<Cart> {\n // Reads reprice and drop unavailable lines but never clamp quantities \u2014\n // silent quantity changes are the write and checkout paths' job, where\n // they come with messaging.\n const reconciledCart = await this.reconcile(cart, {\n clampQuantities: false,\n });\n // Lines are the only thing reconciliation can change; cost derives from them.\n if (JSON.stringify(reconciledCart.lines) !== JSON.stringify(cart.lines)) {\n await saveCartToCookie(reconciledCart, this.config.cookieName);\n }\n return reconciledCart;\n }\n\n async getCart(cartId: string): Promise<Cart | null> {\n try {\n const cart = await loadCartFromCookie(this.config.cookieName);\n return cart?.id === cartId ? cart : null;\n } catch (error) {\n console.warn(\n `${this.config.gatewayName} cart ${cartId} not found, could not fetch cart:`,\n error,\n );\n return null;\n }\n }\n\n async createCart(\n lines?: CartLinePayload[],\n _discountCodes?: string[],\n ): Promise<Cart | null> {\n try {\n const cart = await createCartWithPersistence({\n cookieName: this.config.cookieName,\n lines,\n lineIdGenerator: this.config.lineIdGenerator,\n cartIdGenerator: this.config.cartIdGenerator,\n reconcile: this.reconcileForWrite,\n });\n return cart;\n } catch (createCartError) {\n console.warn(\n `Failed to create ${this.config.gatewayName} cart${lines?.length ? \" with lines\" : \"\"}:`,\n createCartError,\n );\n return null;\n }\n }\n\n async addToCart(cartPayload: AddToCartPayload): Promise<Cart | null> {\n try {\n const cartData = await addToCartWithPersistence(cartPayload, {\n cookieName: this.config.cookieName,\n mergeFn: this.config.lineMergeFn,\n lineIdGenerator: this.config.lineIdGenerator,\n reconcile: this.reconcileForWrite,\n });\n return cartData;\n } catch (error) {\n console.warn(`Failed to add items to ${this.config.gatewayName} cart:`, {\n error: error instanceof Error ? error.message : String(error),\n cartId: cartPayload.cartId,\n itemCount: cartPayload.lines.length,\n });\n return null;\n }\n }\n\n async updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null> {\n try {\n const cartData = await updateCartLineItemWithPersistence(\n cartPayload,\n this.config.cookieName,\n this.reconcileForWrite,\n );\n return cartData;\n } catch (error) {\n console.warn(\n `Failed to update ${this.config.gatewayName} cart line item:`,\n {\n error: error instanceof Error ? error.message : String(error),\n cartId: cartPayload.cartId,\n lineId: cartPayload.line.id,\n quantity: cartPayload.line.quantity,\n },\n );\n return null;\n }\n }\n\n async removeCartLineItems(\n cartPayload: RemoveFromCartPayload,\n ): Promise<Cart | null> {\n try {\n const cartData = await removeCartLineItemsWithPersistence(\n cartPayload,\n this.config.cookieName,\n );\n return cartData;\n } catch (error) {\n console.warn(\n `Failed to remove ${this.config.gatewayName} cart line items:`,\n {\n error: error instanceof Error ? error.message : String(error),\n cartId: cartPayload.cartId,\n lineIds: cartPayload.lineIds,\n },\n );\n return null;\n }\n }\n\n async updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null> {\n console.warn(\n `${this.config.gatewayName} cart gateway does not yet support discount codes`,\n );\n // Return existing cart without changes\n try {\n return await this.getCart(cartPayload.cartId);\n } catch (error) {\n console.warn(\"Failed to fetch cart for discount code update:\", error);\n return null;\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { getCanopyApiGateway } from "../../gateways/canopy-api";
|
|
3
3
|
class ResolverCartGateway {
|
|
4
|
+
// Shopify carts are fetched from Shopify on every read — already reconciled.
|
|
5
|
+
async reconcileStoredCart(cart) {
|
|
6
|
+
return cart;
|
|
7
|
+
}
|
|
4
8
|
// Returns the gateway's wire-parsed Cart where the client Cart is expected.
|
|
5
9
|
// Safe because client = wire + widenings only — enforced by the widening
|
|
6
10
|
// assertions in cart-schema.spec.ts.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/gateways/resolver-cart-gateway.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\nimport type { CartGateway } from \"./cart-gateway\";\n\nimport { getCanopyApiGateway } from \"../../gateways/canopy-api\";\n\n/**\n * @author Ryan\n * CartGateway that delegates all mutations to the canopy-api's cart\n * endpoints. Used for \"externalCart\" integrations (Shopify). canopy-api\n * handles credential resolution and upstream API calls.\n */\nclass ResolverCartGateway implements CartGateway {\n // Returns the gateway's wire-parsed Cart where the client Cart is expected.\n // Safe because client = wire + widenings only \u2014 enforced by the widening\n // assertions in cart-schema.spec.ts.\n async getCart(cartId: string): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartGet({ cartId });\n } catch (error) {\n console.warn(`Cart ${cartId} not found via canopy-api:`, error);\n return null;\n }\n }\n\n async createCart(\n lines?: CartLinePayload[],\n discountCodes?: string[],\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartCreate({\n lines: lines?.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to create cart via canopy-api:\", error);\n return null;\n }\n }\n\n async addToCart(cartPayload: AddToCartPayload): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartAddLines({\n cartId: cartPayload.cartId,\n lines: cartPayload.lines.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n });\n } catch (error) {\n console.warn(\"Failed to add to cart via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateLines({\n cartId: cartPayload.cartId,\n lines: [\n {\n lineId: cartPayload.line.id,\n quantity: cartPayload.line.quantity,\n attributes: cartPayload.line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: cartPayload.line.sellingPlanId,\n },\n ],\n });\n } catch (error) {\n console.warn(\"Failed to update cart line via canopy-api:\", error);\n return null;\n }\n }\n\n async removeCartLineItems(\n cartPayload: RemoveFromCartPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartRemoveLines({\n cartId: cartPayload.cartId,\n lineIds: cartPayload.lineIds,\n });\n } catch (error) {\n console.warn(\"Failed to remove cart lines via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateDiscountCodes({\n cartId: cartPayload.cartId,\n discountCodes: cartPayload.discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to update discount codes via canopy-api:\", error);\n return null;\n }\n }\n}\n\nexport async function createResolverCartGateway(): Promise<CartGateway> {\n return new ResolverCartGateway();\n}\n"],
|
|
5
|
-
"mappings": ";AAYA,SAAS,2BAA2B;AAQpC,MAAM,oBAA2C;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\nimport type { CartGateway } from \"./cart-gateway\";\n\nimport { getCanopyApiGateway } from \"../../gateways/canopy-api\";\n\n/**\n * @author Ryan\n * CartGateway that delegates all mutations to the canopy-api's cart\n * endpoints. Used for \"externalCart\" integrations (Shopify). canopy-api\n * handles credential resolution and upstream API calls.\n */\nclass ResolverCartGateway implements CartGateway {\n // Shopify carts are fetched from Shopify on every read \u2014 already reconciled.\n async reconcileStoredCart(cart: Cart): Promise<Cart> {\n return cart;\n }\n\n // Returns the gateway's wire-parsed Cart where the client Cart is expected.\n // Safe because client = wire + widenings only \u2014 enforced by the widening\n // assertions in cart-schema.spec.ts.\n async getCart(cartId: string): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartGet({ cartId });\n } catch (error) {\n console.warn(`Cart ${cartId} not found via canopy-api:`, error);\n return null;\n }\n }\n\n async createCart(\n lines?: CartLinePayload[],\n discountCodes?: string[],\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartCreate({\n lines: lines?.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to create cart via canopy-api:\", error);\n return null;\n }\n }\n\n async addToCart(cartPayload: AddToCartPayload): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartAddLines({\n cartId: cartPayload.cartId,\n lines: cartPayload.lines.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n });\n } catch (error) {\n console.warn(\"Failed to add to cart via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateLines({\n cartId: cartPayload.cartId,\n lines: [\n {\n lineId: cartPayload.line.id,\n quantity: cartPayload.line.quantity,\n attributes: cartPayload.line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: cartPayload.line.sellingPlanId,\n },\n ],\n });\n } catch (error) {\n console.warn(\"Failed to update cart line via canopy-api:\", error);\n return null;\n }\n }\n\n async removeCartLineItems(\n cartPayload: RemoveFromCartPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartRemoveLines({\n cartId: cartPayload.cartId,\n lineIds: cartPayload.lineIds,\n });\n } catch (error) {\n console.warn(\"Failed to remove cart lines via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateDiscountCodes({\n cartId: cartPayload.cartId,\n discountCodes: cartPayload.discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to update discount codes via canopy-api:\", error);\n return null;\n }\n }\n}\n\nexport async function createResolverCartGateway(): Promise<CartGateway> {\n return new ResolverCartGateway();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -13,6 +13,20 @@ declare class StripeCartGateway extends CookieBasedCartGateway {
|
|
|
13
13
|
lineIdGenerator: typeof stripeLineIdGenerator;
|
|
14
14
|
cartIdGenerator: typeof stripeCartIdGenerator;
|
|
15
15
|
gatewayName: string;
|
|
16
|
+
reconcileLines: (lines: {
|
|
17
|
+
variantId: string;
|
|
18
|
+
quantity: number;
|
|
19
|
+
}[]) => Promise<({
|
|
20
|
+
variantId: string;
|
|
21
|
+
status: "purchasable";
|
|
22
|
+
unitPriceMinor: number;
|
|
23
|
+
compareAtPriceMinor: number | null;
|
|
24
|
+
currencyCode: string;
|
|
25
|
+
purchasableQuantity: number;
|
|
26
|
+
} | {
|
|
27
|
+
variantId: string;
|
|
28
|
+
status: "unavailable";
|
|
29
|
+
})[]>;
|
|
16
30
|
};
|
|
17
31
|
}
|
|
18
32
|
export declare function createStripeCartGateway(): Promise<StripeCartGateway>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use server";
|
|
2
|
+
import { getCanopyApiGateway } from "../../gateways/canopy-api";
|
|
2
3
|
import { REPLO_CART_COOKIE_NAME } from "./cart-constants";
|
|
3
4
|
import { CookieBasedCartGateway } from "./cookie-cart-gateway";
|
|
4
5
|
function stripeLineMergeFn(existingLines, newLine) {
|
|
@@ -16,7 +17,11 @@ class StripeCartGateway extends CookieBasedCartGateway {
|
|
|
16
17
|
lineMergeFn: stripeLineMergeFn,
|
|
17
18
|
lineIdGenerator: stripeLineIdGenerator,
|
|
18
19
|
cartIdGenerator: stripeCartIdGenerator,
|
|
19
|
-
gatewayName: "Stripe"
|
|
20
|
+
gatewayName: "Stripe",
|
|
21
|
+
reconcileLines: async (lines) => {
|
|
22
|
+
const gateway = await getCanopyApiGateway();
|
|
23
|
+
return await gateway.reconcileCartLines({ lines });
|
|
24
|
+
}
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
async function createStripeCartGateway() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/gateways/stripe-cart-gateway.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type { CartLine, CartLinePayload } from \"../cart-types\";\n\nimport { REPLO_CART_COOKIE_NAME } from \"./cart-constants\";\nimport { CookieBasedCartGateway } from \"./cookie-cart-gateway\";\n\n/**\n * Merges lines by merchandise ID only (unlike default which also checks selling plan)\n */\nfunction stripeLineMergeFn(\n existingLines: CartLine[],\n newLine: CartLinePayload,\n): number {\n return existingLines.findIndex((line) => line.merchandise?.id === newLine.id);\n}\n\nfunction stripeLineIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nfunction stripeCartIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nclass StripeCartGateway extends CookieBasedCartGateway {\n protected readonly config = {\n cookieName: REPLO_CART_COOKIE_NAME,\n lineMergeFn: stripeLineMergeFn,\n lineIdGenerator: stripeLineIdGenerator,\n cartIdGenerator: stripeCartIdGenerator,\n gatewayName: \"Stripe\",\n };\n}\n\nexport async function createStripeCartGateway(): Promise<StripeCartGateway> {\n return new StripeCartGateway();\n}\n"],
|
|
5
|
-
"mappings": ";AAIA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAKvC,SAAS,kBACP,eACA,SACQ;AACR,SAAO,cAAc,UAAU,CAAC,SAAS,KAAK,aAAa,OAAO,QAAQ,EAAE;AAC9E;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,MAAM,0BAA0B,uBAAuB;AAAA,EAClC,SAAS;AAAA,IAC1B,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,aAAa;AAAA,
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type { CartLine, CartLinePayload } from \"../cart-types\";\n\nimport { getCanopyApiGateway } from \"../../gateways/canopy-api\";\nimport { REPLO_CART_COOKIE_NAME } from \"./cart-constants\";\nimport { CookieBasedCartGateway } from \"./cookie-cart-gateway\";\n\n/**\n * Merges lines by merchandise ID only (unlike default which also checks selling plan)\n */\nfunction stripeLineMergeFn(\n existingLines: CartLine[],\n newLine: CartLinePayload,\n): number {\n return existingLines.findIndex((line) => line.merchandise?.id === newLine.id);\n}\n\nfunction stripeLineIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nfunction stripeCartIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nclass StripeCartGateway extends CookieBasedCartGateway {\n protected readonly config = {\n cookieName: REPLO_CART_COOKIE_NAME,\n lineMergeFn: stripeLineMergeFn,\n lineIdGenerator: stripeLineIdGenerator,\n cartIdGenerator: stripeCartIdGenerator,\n gatewayName: \"Stripe\",\n reconcileLines: async (\n lines: { variantId: string; quantity: number }[],\n ) => {\n const gateway = await getCanopyApiGateway();\n return await gateway.reconcileCartLines({ lines });\n },\n };\n}\n\nexport async function createStripeCartGateway(): Promise<StripeCartGateway> {\n return new StripeCartGateway();\n}\n"],
|
|
5
|
+
"mappings": ";AAIA,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAKvC,SAAS,kBACP,eACA,SACQ;AACR,SAAO,cAAc,UAAU,CAAC,SAAS,KAAK,aAAa,OAAO,QAAQ,EAAE;AAC9E;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,MAAM,0BAA0B,uBAAuB;AAAA,EAClC,SAAS;AAAA,IAC1B,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,gBAAgB,OACd,UACG;AACH,YAAM,UAAU,MAAM,oBAAoB;AAC1C,aAAO,MAAM,QAAQ,mBAAmB,EAAE,MAAM,CAAC;AAAA,IACnD;AAAA,EACF;AACF;AAEA,eAAsB,0BAAsD;AAC1E,SAAO,IAAI,kBAAkB;AAC/B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { QueryClient } from "@tanstack/react-query";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CartError } from "../cart-provider";
|
|
3
|
+
import type { CartLineAdjustment, CartLineAttribute, CartLineMerchandise } from "../cart-types";
|
|
3
4
|
/**
|
|
4
5
|
* Describes a single item to add to the cart. Uses the merchandiseId
|
|
5
6
|
* (e.g. a variant GID) directly from data loader output. Product
|
|
@@ -19,6 +20,18 @@ export interface AddToCartItem {
|
|
|
19
20
|
properties?: CartLineAttribute[];
|
|
20
21
|
merchandise?: CartLineMerchandise;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* What an `addToCart` call did. `adjustments` lists what the server changed
|
|
25
|
+
* about a saved request (a quantity clamped to stock, an unavailable line
|
|
26
|
+
* removed). `error` is set instead when the cart could not be created or
|
|
27
|
+
* saved; the same message is on `useCart().error` for the cart UI.
|
|
28
|
+
*/
|
|
29
|
+
export interface AddToCartResult {
|
|
30
|
+
/** What the server changed about a saved add; empty when nothing was */
|
|
31
|
+
adjustments: CartLineAdjustment[];
|
|
32
|
+
/** Why the add was not saved; null on success */
|
|
33
|
+
error: CartError | null;
|
|
34
|
+
}
|
|
22
35
|
export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId, }: {
|
|
23
36
|
queryClient: QueryClient;
|
|
24
37
|
merchandiseId: string;
|
|
@@ -37,6 +50,12 @@ export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId
|
|
|
37
50
|
* merchandiseId. Pass `merchandise` explicitly only when the variant is
|
|
38
51
|
* known but its product was not loaded via a supported loader.
|
|
39
52
|
*
|
|
53
|
+
* `addToCart` never throws. It resolves an `AddToCartResult`: `adjustments`
|
|
54
|
+
* when the add was saved, or `error` when the cart could not be created or
|
|
55
|
+
* saved. On failure the provider also records the error on `useCart().error`
|
|
56
|
+
* and opens the cart UI, which renders it, so a click handler needs no
|
|
57
|
+
* handling of its own.
|
|
58
|
+
*
|
|
40
59
|
* @example
|
|
41
60
|
* ```tsx
|
|
42
61
|
* import { useAddToCart } from "@replohq/sdk/cart/hooks/use-add-to-cart";
|
|
@@ -48,7 +67,7 @@ export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId
|
|
|
48
67
|
* <button
|
|
49
68
|
* disabled={!variant.availableForSale || isAdding}
|
|
50
69
|
* onClick={() => {
|
|
51
|
-
* addToCart([{ merchandiseId: variant.id, quantity: 1 }]);
|
|
70
|
+
* void addToCart([{ merchandiseId: variant.id, quantity: 1 }]);
|
|
52
71
|
* }}
|
|
53
72
|
* >
|
|
54
73
|
* {isAdding ? "Adding..." : "Add to Cart"}
|
|
@@ -58,6 +77,6 @@ export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId
|
|
|
58
77
|
* ```
|
|
59
78
|
*/
|
|
60
79
|
export declare function useAddToCart(): {
|
|
61
|
-
addToCart: (items: AddToCartItem[], openCartAfterAdd?: boolean) => Promise<
|
|
80
|
+
addToCart: (items: AddToCartItem[], openCartAfterAdd?: boolean) => Promise<AddToCartResult>;
|
|
62
81
|
isAdding: boolean;
|
|
63
82
|
};
|
|
@@ -5,6 +5,7 @@ import { fromMajorUnitsToMinorUnits } from "../../_vendor/schemas/money.mjs";
|
|
|
5
5
|
import { DATA_LOADER_KEYS } from "../../loaders/loader-keys";
|
|
6
6
|
import { useCartInternal } from "../cart-provider";
|
|
7
7
|
import { REPLO_ATTRIBUTION_PROPERTY } from "../cart-types";
|
|
8
|
+
import { toCartError } from "../utils/cart-error";
|
|
8
9
|
const PRODUCT_LOADER_KEYS = [
|
|
9
10
|
DATA_LOADER_KEYS.SHOPIFY_PRODUCT,
|
|
10
11
|
DATA_LOADER_KEYS.REPLO_PRODUCT
|
|
@@ -108,7 +109,13 @@ function useAddToCart() {
|
|
|
108
109
|
merchandise
|
|
109
110
|
};
|
|
110
111
|
});
|
|
111
|
-
await cart.addToCart(
|
|
112
|
+
const { adjustments } = await cart.addToCart(
|
|
113
|
+
cartLines,
|
|
114
|
+
openCartAfterAdd
|
|
115
|
+
);
|
|
116
|
+
return { adjustments, error: null };
|
|
117
|
+
} catch (caught) {
|
|
118
|
+
return { adjustments: [], error: toCartError("add", caught) };
|
|
112
119
|
} finally {
|
|
113
120
|
setIsAdding(false);
|
|
114
121
|
}
|