@techsio/storefront-data
Version:
Medusa-first storefront data layer built on TanStack Query.
196 lines (195 loc) • 7.66 kB
JavaScript
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 };