@techsio/storefront-data
Version:
Medusa-first storefront data layer built on TanStack Query.
324 lines (323 loc) • 15.4 kB
JavaScript
import { useMutation, useQueries, useQuery, useQueryClient, useSuspenseQueries, useSuspenseQuery } from "@tanstack/react-query";
import { createCacheConfig, getPrefetchCacheOptions } from "../shared/cache-config.js";
import { getCachedCartById, patchCartCaches, syncCartCaches } from "../shared/cart-cache-sync.js";
import { createCheckoutQueryKeys } from "./query-keys.js";
function createCheckoutHooks({ service, queryKeys, queryKeyNamespace = "storefront-data", cacheConfig, cartQueryKeys, isActiveCartQueryKey }) {
const resolvedCacheConfig = cacheConfig ?? createCacheConfig();
const resolvedQueryKeys = queryKeys ?? createCheckoutQueryKeys(queryKeyNamespace);
const cartCacheOptions = isActiveCartQueryKey ? {
isActiveCartQueryKey
} : void 0;
const getPaymentProvidersQueryOptions = (regionId)=>({
queryKey: resolvedQueryKeys.paymentProviders(regionId),
queryFn: ({ signal })=>service.listPaymentProviders(regionId, signal),
...resolvedCacheConfig.semiStatic
});
function fetchPaymentProviders(queryClient, regionId) {
const queryOptions = getPaymentProvidersQueryOptions(regionId);
return queryClient.fetchQuery({
queryKey: queryOptions.queryKey,
queryFn: queryOptions.queryFn,
...getPrefetchCacheOptions(resolvedCacheConfig, "semiStatic")
});
}
const buildCalculatedById = (calculatedOptions, calculatedQueries)=>{
const calculatedById = new Map();
for (const [index, query] of calculatedQueries.entries()){
const option = calculatedOptions[index];
if (option && query.data) calculatedById.set(option.id, query.data);
}
return calculatedById;
};
const buildShippingPrices = (shippingOptions, calculatedById)=>{
const shippingPrices = {};
for (const option of shippingOptions){
if ("calculated" === option.price_type) {
const calculated = calculatedById.get(option.id);
if (calculated && "number" == typeof calculated.amount) shippingPrices[option.id] = calculated.amount;
continue;
}
if ("number" == typeof option.amount) shippingPrices[option.id] = option.amount;
}
return shippingPrices;
};
const patchPaymentCollection = (cart, paymentCollection)=>({
...cart,
payment_collection: paymentCollection
});
function useShippingMethodMutation(cartId, options) {
const queryClient = useQueryClient();
const onMutate = options?.onMutate;
return useMutation({
mutationFn: ({ optionId, data })=>{
if (!cartId) throw new Error("Cart id is required");
return service.addShippingMethod(cartId, optionId, data);
},
onMutate: onMutate ? async (variables)=>onMutate(variables) : void 0,
onSuccess: (cart, variables, context)=>{
if (cartQueryKeys) syncCartCaches(queryClient, cartQueryKeys, cart);
options?.onSuccess?.(cart, variables, context);
},
onError: (error, variables, context)=>{
options?.onError?.(error, variables, context);
},
onSettled: (data, error, variables, context)=>{
options?.onSettled?.(data, error, variables, context);
}
});
}
function usePaymentMutation(cartId, options) {
const queryClient = useQueryClient();
const onMutate = options?.onMutate;
return useMutation({
mutationFn: (providerId)=>{
if (!cartId) throw new Error("Cart id is required");
return service.initiatePaymentSession(cartId, providerId);
},
onMutate: onMutate ? async (variables)=>onMutate(variables) : void 0,
onSuccess: (data, variables, context)=>{
if (cartQueryKeys && cartId) {
patchCartCaches(queryClient, cartQueryKeys, cartId, {
patch: (cached)=>patchPaymentCollection(cached, data)
});
queryClient.invalidateQueries({
queryKey: cartQueryKeys.all()
});
}
options?.onSuccess?.(data, variables, context);
},
onError: (error, variables, context)=>{
options?.onError?.(error, variables, context);
},
onSettled: (data, error, variables, context)=>{
options?.onSettled?.(data, error, variables, context);
}
});
}
const resolvePaymentState = (cart)=>{
const hasShippingMethod = (cart?.shipping_methods?.length ?? 0) > 0;
const hasPaymentCollection = Boolean(cart?.payment_collection);
const hasPaymentSessions = (cart?.payment_collection?.payment_sessions?.length ?? 0) > 0;
return {
hasShippingMethod,
hasPaymentCollection,
hasPaymentSessions
};
};
function useReactiveCart(inputCart, cartId) {
const queryClient = useQueryClient();
const canSubscribeToCart = Boolean(cartId && cartQueryKeys);
const fallbackReactiveCartKey = [
...resolvedQueryKeys.all(),
"reactive-cart",
cartId ?? "unknown"
];
const readCachedCart = ()=>canSubscribeToCart && cartId && cartQueryKeys ? getCachedCartById(queryClient, cartQueryKeys, cartId, cartCacheOptions) : null;
const initialCart = readCachedCart() ?? inputCart ?? null;
const { data: cachedCart = initialCart } = useQuery({
queryKey: canSubscribeToCart && cartId && cartQueryKeys ? cartQueryKeys.detail(cartId) : fallbackReactiveCartKey,
queryFn: readCachedCart,
enabled: canSubscribeToCart,
initialData: initialCart,
staleTime: 1 / 0,
gcTime: 1 / 0,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false
});
return cachedCart ?? inputCart ?? null;
}
function useCheckoutShipping(input, options) {
const cartId = input.cartId;
const enabled = input.enabled ?? Boolean(cartId);
const calculatePrices = input.calculatePrices ?? true;
const cacheKey = input.cacheKey;
const effectiveCart = useReactiveCart(input.cart, cartId);
const { data: shippingOptions = [], isLoading, isFetching } = useQuery({
queryKey: resolvedQueryKeys.shippingOptions(cartId ?? "unknown", cacheKey),
queryFn: ({ signal })=>{
if (!cartId) return [];
return service.listShippingOptions(cartId, signal);
},
enabled,
...resolvedCacheConfig.realtime
});
const calculatedOptions = shippingOptions.filter((option)=>"calculated" === option.price_type);
const calculateShippingOption = service.calculateShippingOption;
const shouldCalculate = Boolean(cartId) && calculatePrices && "function" == typeof calculateShippingOption;
const cartIdValue = cartId ?? "";
const calculatedQueries = useQueries({
queries: shouldCalculate ? calculatedOptions.map((option)=>{
const data = input.buildShippingData?.(option);
return {
queryKey: resolvedQueryKeys.shippingOptionPrice({
cartId: cartIdValue,
optionId: option.id,
data
}),
queryFn: ({ signal })=>calculateShippingOption(option.id, {
cart_id: cartIdValue,
data
}, signal),
enabled,
...resolvedCacheConfig.realtime
};
}) : []
});
const calculatedById = buildCalculatedById(calculatedOptions, calculatedQueries);
const shippingPrices = buildShippingPrices(shippingOptions, calculatedById);
const { mutate: mutateShippingMethod, mutateAsync: mutateShippingMethodAsync, isPending: isSettingShipping } = useShippingMethodMutation(cartId, options);
const setShippingMethod = (optionId, data)=>{
mutateShippingMethod({
optionId,
data
});
};
const setShippingMethodAsync = (optionId, data)=>mutateShippingMethodAsync({
optionId,
data
});
const selectedShippingMethodId = effectiveCart?.shipping_methods?.[0]?.shipping_option_id;
const selectedShippingMethodData = effectiveCart?.shipping_methods?.[0]?.data;
const selectedOption = shippingOptions.find((option)=>option.id === selectedShippingMethodId);
return {
shippingOptions,
shippingPrices,
isLoading,
isFetching,
isCalculating: calculatedQueries.some((query)=>query.isFetching),
setShippingMethod,
setShippingMethodAsync,
isSettingShipping,
selectedShippingMethodId,
selectedShippingMethodData,
selectedOption
};
}
function useSuspenseCheckoutShipping(input, options) {
const cartId = input.cartId;
if (!cartId) throw new Error("Cart id is required for checkout shipping");
const calculatePrices = input.calculatePrices ?? true;
const cacheKey = input.cacheKey;
const effectiveCart = useReactiveCart(input.cart, cartId);
const { data: shippingOptions, isFetching } = useSuspenseQuery({
queryKey: resolvedQueryKeys.shippingOptions(cartId, cacheKey),
queryFn: ({ signal })=>service.listShippingOptions(cartId, signal),
...resolvedCacheConfig.realtime
});
const calculatedOptions = shippingOptions.filter((option)=>"calculated" === option.price_type);
const calculateShippingOption = service.calculateShippingOption;
const shouldCalculate = calculatePrices && "function" == typeof calculateShippingOption;
const calculatedQueries = useSuspenseQueries({
queries: shouldCalculate ? calculatedOptions.map((option)=>{
const data = input.buildShippingData?.(option);
return {
queryKey: resolvedQueryKeys.shippingOptionPrice({
cartId,
optionId: option.id,
data
}),
queryFn: ({ signal })=>calculateShippingOption(option.id, {
cart_id: cartId,
data
}, signal),
...resolvedCacheConfig.realtime
};
}) : []
});
const calculatedById = buildCalculatedById(calculatedOptions, calculatedQueries);
const shippingPrices = buildShippingPrices(shippingOptions, calculatedById);
const { mutate: mutateShippingMethod, mutateAsync: mutateShippingMethodAsync, isPending: isSettingShipping } = useShippingMethodMutation(cartId, options);
const setShippingMethod = (optionId, data)=>{
mutateShippingMethod({
optionId,
data
});
};
const setShippingMethodAsync = (optionId, data)=>mutateShippingMethodAsync({
optionId,
data
});
const selectedShippingMethodId = effectiveCart?.shipping_methods?.[0]?.shipping_option_id;
const selectedShippingMethodData = effectiveCart?.shipping_methods?.[0]?.data;
const selectedOption = shippingOptions.find((option)=>option.id === selectedShippingMethodId);
return {
shippingOptions,
shippingPrices,
isLoading: false,
isFetching,
isCalculating: calculatedQueries.some((query)=>query.isFetching),
setShippingMethod,
setShippingMethodAsync,
isSettingShipping,
selectedShippingMethodId,
selectedShippingMethodData,
selectedOption
};
}
function useCheckoutPayment(input, options) {
const cartId = input.cartId;
const effectiveCart = useReactiveCart(input.cart, cartId);
const resolvedCartId = cartId ?? effectiveCart?.id;
const regionId = input.regionId ?? effectiveCart?.region_id;
const enabled = input.enabled ?? Boolean(regionId);
const paymentProvidersQueryOptions = regionId ? getPaymentProvidersQueryOptions(regionId) : {
queryKey: resolvedQueryKeys.paymentProviders("unknown"),
queryFn: async ()=>[],
...resolvedCacheConfig.semiStatic
};
const { data: paymentProviders = [], isLoading, isFetching } = useQuery({
...paymentProvidersQueryOptions,
enabled
});
const { mutate: initiatePayment, mutateAsync: initiatePaymentAsync, isPending: isInitiatingPayment } = usePaymentMutation(resolvedCartId, options);
const paymentState = resolvePaymentState(effectiveCart);
const canInitiatePayment = Boolean(resolvedCartId && paymentState.hasShippingMethod);
return {
paymentProviders,
initiatePayment,
initiatePaymentAsync,
isInitiatingPayment,
isLoading,
isFetching,
canInitiatePayment,
hasPaymentCollection: paymentState.hasPaymentCollection,
hasPaymentSessions: paymentState.hasPaymentSessions
};
}
function useSuspenseCheckoutPayment(input, options) {
const cartId = input.cartId;
const effectiveCart = useReactiveCart(input.cart, cartId);
const resolvedCartId = cartId ?? effectiveCart?.id;
const regionId = input.regionId ?? effectiveCart?.region_id;
const paymentProvidersQueryOptions = regionId ? getPaymentProvidersQueryOptions(regionId) : {
queryKey: resolvedQueryKeys.paymentProviders("unknown"),
queryFn: async ()=>[],
...resolvedCacheConfig.semiStatic
};
const { data: paymentProviders, isFetching } = useSuspenseQuery(paymentProvidersQueryOptions);
const { mutate: initiatePayment, mutateAsync: initiatePaymentAsync, isPending: isInitiatingPayment } = usePaymentMutation(resolvedCartId, options);
const paymentState = resolvePaymentState(effectiveCart);
const canInitiatePayment = Boolean(resolvedCartId && paymentState.hasShippingMethod);
return {
paymentProviders,
initiatePayment,
initiatePaymentAsync,
isInitiatingPayment,
isLoading: false,
isFetching,
canInitiatePayment,
hasPaymentCollection: paymentState.hasPaymentCollection,
hasPaymentSessions: paymentState.hasPaymentSessions
};
}
return {
useCheckoutShipping,
useSuspenseCheckoutShipping,
useCheckoutPayment,
useSuspenseCheckoutPayment,
getPaymentProvidersQueryOptions,
fetchPaymentProviders
};
}
export { createCheckoutHooks };