UNPKG

@techsio/storefront-data

Version:

Medusa-first storefront data layer built on TanStack Query.

196 lines (195 loc) • 7.66 kB
import { useQueryClient } from "@tanstack/react-query"; import { createDefaultActiveCartQueryMatcher, invalidateCartCaches, syncCartCaches } from "../shared/cart-cache-sync.js"; import { toErrorWithCode } from "../shared/error-utils.js"; const toCartMutationError = (error)=>toErrorWithCode(error, "An unknown error occurred"); const isRenderableCartItem = (item)=>{ if (!item || "object" != typeof item) return false; const candidate = item; return "string" == typeof candidate.id && "number" == typeof candidate.quantity; }; const isRenderableCart = (cart)=>{ if (!Array.isArray(cart.items)) return false; return cart.items.every(isRenderableCartItem); }; function createMedusaCartFlow({ storefront, cartStorage, isActiveCartQueryKey: customActiveCartQueryKeyMatcher }) { const cartHooks = storefront.hooks.cart; const cartQueryKeys = storefront.queryKeys.cart; const checkoutQueryKeys = storefront.queryKeys.checkout; const orderQueryKeys = storefront.queryKeys.orders; const isActiveCartQueryKey = customActiveCartQueryKeyMatcher ?? createDefaultActiveCartQueryMatcher(cartQueryKeys); const retrieveCartById = (cartId, signal)=>storefront.services.cart.retrieveCart(cartId, signal); const fetchCanonicalCart = async (cartId)=>{ try { return await retrieveCartById(cartId); } catch { return null; } }; const resolveRenderableCart = async (cart)=>{ if (isRenderableCart(cart)) return cart; const canonicalCart = await fetchCanonicalCart(cart.id); if (canonicalCart && isRenderableCart(canonicalCart)) return canonicalCart; return cart; }; const clearCompletedCart = (queryClient, cartId)=>{ if (!cartId) return; if (cartStorage?.get() === cartId) cartStorage.clear(); queryClient.removeQueries({ predicate: (query)=>isActiveCartQueryKey(query.queryKey, cartId) }); queryClient.removeQueries({ queryKey: cartQueryKeys.detail(cartId) }); queryClient.removeQueries({ queryKey: checkoutQueryKeys.shippingOptions(cartId) }); }; const buildMutationHandlers = (queryClient, options)=>({ onSuccess: async (cart)=>{ const resolvedCart = await resolveRenderableCart(cart); if (resolvedCart !== cart) { syncCartCaches(queryClient, cartQueryKeys, resolvedCart, { isActiveCartQueryKey }); invalidateCartCaches(queryClient, cartQueryKeys, resolvedCart.id, { isActiveCartQueryKey }); } options?.onSuccess?.(resolvedCart); }, onError: (error)=>{ options?.onError?.(toCartMutationError(error)); } }); const normalizeCartMutation = (mutation)=>{ const mutate = (input, mutateOptions)=>{ mutation.mutate(input, { onSuccess: async (cart)=>{ const resolvedCart = await resolveRenderableCart(cart); mutateOptions?.onSuccess?.(resolvedCart); }, onError: (error)=>{ mutateOptions?.onError?.(toCartMutationError(error)); } }); }; const mutateAsync = async (input)=>{ try { const cart = await mutation.mutateAsync(input); return await resolveRenderableCart(cart); } catch (error) { throw toCartMutationError(error); } }; return { ...mutation, mutate, mutateAsync }; }; function useCart(input) { const { cart, isLoading, error, itemCount, isEmpty, hasItems } = cartHooks.useCart({ autoCreate: false, autoUpdateRegion: false, ...input ?? {} }); return { cart, isLoading, isError: Boolean(error), error: error ? new Error(error) : null, itemCount, isEmpty, hasItems }; } function useSuspenseCart(input) { const { cart, itemCount, isEmpty, hasItems } = cartHooks.useSuspenseCart({ autoCreate: false, autoUpdateRegion: false, ...input ?? {} }); return { cart, itemCount, isEmpty, hasItems }; } function useAddToCart(options) { const queryClient = useQueryClient(); const mutation = cartHooks.useAddLineItem(buildMutationHandlers(queryClient, options)); return normalizeCartMutation(mutation); } function useUpdateLineItem(options) { const queryClient = useQueryClient(); const mutation = cartHooks.useUpdateLineItem(buildMutationHandlers(queryClient, options)); return normalizeCartMutation(mutation); } function useRemoveLineItem(options) { const queryClient = useQueryClient(); const mutation = cartHooks.useRemoveLineItem(buildMutationHandlers(queryClient, options)); return normalizeCartMutation(mutation); } const getCompletedCartIdFromContext = (context)=>{ if (!(context && "object" == typeof context && "completedCartId" in context)) return; const completedCartId = context.completedCartId; if ("string" == typeof completedCartId || null === completedCartId) return completedCartId; }; const handleOrderCompletionSuccess = ({ queryClient, order, variables, context, onSuccess })=>{ const completedCartId = getCompletedCartIdFromContext(context) ?? variables.cartId ?? null; clearCompletedCart(queryClient, completedCartId); queryClient.invalidateQueries({ queryKey: cartQueryKeys.all() }); queryClient.invalidateQueries({ queryKey: checkoutQueryKeys.all() }); if (order.region_id) queryClient.invalidateQueries({ queryKey: checkoutQueryKeys.paymentProviders(order.region_id) }); queryClient.setQueryData(orderQueryKeys.detail({ id: order.id }), order); queryClient.invalidateQueries({ queryKey: orderQueryKeys.all() }); onSuccess?.(order); }; function useCompleteCart(options) { const queryClient = useQueryClient(); return cartHooks.useCompleteCart({ onMutate: (variables)=>({ completedCartId: variables.cartId ?? cartStorage?.get() ?? null }), onSuccess: (result, variables, context)=>{ if ("order" !== result.type) { syncCartCaches(queryClient, cartQueryKeys, result.cart, { isActiveCartQueryKey }); options?.onError?.(result.error, result.cart); return; } handleOrderCompletionSuccess({ queryClient, order: result.order, variables, context, onSuccess: options?.onSuccess }); }, onError: (error)=>{ options?.onRequestError?.(error); } }); } return { useCart, useSuspenseCart, useAddToCart, useUpdateLineItem, useRemoveLineItem, useCompleteCart }; } export { createMedusaCartFlow };