UNPKG

@techsio/storefront-data

Version:

Medusa-first storefront data layer built on TanStack Query.

324 lines (323 loc) • 15.4 kB
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 };