@techsio/storefront-data
Version:
Medusa-first storefront data layer built on TanStack Query.
232 lines (231 loc) • 10.6 kB
JavaScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getCachedCartById } from "../shared/cart-cache-sync.js";
import { resolveCheckoutCartInput, resolveEffectiveCheckoutCart, resolveExistingPaymentCollection, resolveSelectedPaymentProviderId } from "../shared/checkout-flow-utils.js";
import { createErrorWithStage } from "../shared/error-utils.js";
import { toPlainRecord } from "../shared/object-utils.js";
import { createMedusaCartFlow } from "./cart-flow.js";
const defaultNormalizeShippingData = (data)=>{
if (!data) return;
const entries = Object.entries(data).filter(([, value])=>null != value && "" !== value);
if (0 === entries.length) return;
return Object.fromEntries(entries);
};
const toComparableShippingData = (data)=>JSON.stringify(Object.entries(data ?? {}).filter(([, value])=>null != value && "" !== value).sort(([left], [right])=>left.localeCompare(right)));
const isSameShippingSelection = ({ selectedOptionId, nextOptionId, nextData, currentData })=>{
if (selectedOptionId !== nextOptionId) return false;
return toComparableShippingData(nextData) === toComparableShippingData(toPlainRecord(currentData));
};
const defaultResolvePaymentProviderId = ({ requestedPaymentProviderId, existingPaymentProviderId, paymentProviders })=>requestedPaymentProviderId ?? existingPaymentProviderId ?? paymentProviders[0]?.id ?? null;
const resolvePaymentProviderSelection = (context, resolvePaymentProviderId)=>{
const resolvedPaymentProviderId = resolvePaymentProviderId?.(context);
if (void 0 !== resolvedPaymentProviderId) return {
paymentProviderId: resolvedPaymentProviderId,
wasExplicit: true
};
return {
paymentProviderId: defaultResolvePaymentProviderId(context),
wasExplicit: false
};
};
const resolveCheckoutEffectiveCart = ({ cartId, cart, getCachedCart })=>resolveEffectiveCheckoutCart({
cartId,
cart,
getCachedCart
});
const ensureCheckoutPaymentCollection = ({ effectiveCart, paymentProviderId, initiatePaymentAsync })=>{
const existingPaymentCollection = resolveExistingPaymentCollection(effectiveCart, paymentProviderId);
if (existingPaymentCollection) return Promise.resolve(existingPaymentCollection);
return initiatePaymentAsync(paymentProviderId);
};
const completeCheckoutOrder = async ({ cartId, completeCartMutation })=>{
const result = await completeCartMutation({
cartId
});
if ("order" !== result.type) throw result.error;
return result.order;
};
function createMedusaCheckoutFlow({ storefront, cartStorage, isActiveCartQueryKey }) {
const checkoutHooks = storefront.hooks.checkout;
const cartFlow = createMedusaCartFlow({
storefront,
cartStorage,
isActiveCartQueryKey
});
function useCheckoutShipping(cartId, cart, options) {
const { resolvedCartId, normalizedCart } = resolveCheckoutCartInput({
cartId,
cart
});
const normalizeShippingData = options?.normalizeShippingData ?? defaultNormalizeShippingData;
const canLoadShipping = Boolean(resolvedCartId);
const shipping = checkoutHooks.useCheckoutShipping({
cartId: resolvedCartId,
cart: normalizedCart,
enabled: options?.enabled ?? canLoadShipping,
calculatePrices: options?.calculatePrices
}, {
onSuccess: (updatedCart)=>{
options?.onSuccess?.(updatedCart);
},
onError: (error)=>{
options?.onError?.(error);
}
});
const setShipping = (optionId, data)=>{
const cleanedData = normalizeShippingData(data);
const currentData = shipping.selectedShippingMethodId === optionId ? shipping.selectedShippingMethodData : void 0;
if (isSameShippingSelection({
selectedOptionId: shipping.selectedShippingMethodId,
nextOptionId: optionId,
nextData: cleanedData,
currentData
})) return;
shipping.setShippingMethod(optionId, cleanedData);
};
const { shippingOptions, shippingPrices, isLoading, isFetching, isCalculating, isSettingShipping, selectedShippingMethodId, selectedOption } = shipping;
return {
shippingOptions,
shippingPrices,
isLoading,
isFetching,
isCalculating,
setShipping,
isSettingShipping,
canLoadShipping,
canSetShipping: shippingOptions.length > 0,
selectedShippingMethodId,
selectedOption
};
}
function useCheckoutPayment(cartId, regionId, cart, options) {
const { resolvedCartId, normalizedCart } = resolveCheckoutCartInput({
cartId,
cart
});
const payment = checkoutHooks.useCheckoutPayment({
cartId: resolvedCartId,
regionId,
cart: normalizedCart,
enabled: options?.enabled ?? Boolean(resolvedCartId)
}, {
onSuccess: (paymentCollection)=>{
options?.onSuccess?.(paymentCollection);
},
onError: (error)=>{
options?.onError?.(error);
}
});
const { paymentProviders, initiatePayment, initiatePaymentAsync, isInitiatingPayment, isLoading, isFetching, canInitiatePayment, hasPaymentCollection, hasPaymentSessions } = payment;
return {
paymentProviders,
initiatePayment,
initiatePaymentAsync,
isInitiatingPayment,
isLoading,
isFetching,
canInitiatePayment,
hasPaymentCollection,
hasPaymentSessions
};
}
const resolveCheckoutPaymentProviderId = async ({ effectiveCart, effectiveRegionId, requestedPaymentProviderId, paymentProviders, resolvePaymentProviderId, queryClient })=>{
const existingPaymentProviderId = resolveSelectedPaymentProviderId(effectiveCart);
const initialSelection = resolvePaymentProviderSelection({
cart: effectiveCart,
existingPaymentProviderId,
paymentProviders,
requestedPaymentProviderId
}, resolvePaymentProviderId);
if (null != initialSelection.paymentProviderId) return initialSelection.paymentProviderId;
if (initialSelection.wasExplicit || !effectiveRegionId) throw new Error("No payment provider available");
const refreshedPaymentProviders = await checkoutHooks.fetchPaymentProviders(queryClient, effectiveRegionId);
const refreshedSelection = resolvePaymentProviderSelection({
cart: effectiveCart,
existingPaymentProviderId,
paymentProviders: refreshedPaymentProviders,
requestedPaymentProviderId
}, resolvePaymentProviderId);
if (!refreshedSelection.paymentProviderId) throw new Error("No payment provider available");
return refreshedSelection.paymentProviderId;
};
function useCompleteCheckout(input, options) {
const queryClient = useQueryClient();
const { resolvedCartId, normalizedCart } = resolveCheckoutCartInput({
cartId: input.cartId,
cart: input.cart
});
const payment = useCheckoutPayment(resolvedCartId, input.regionId, normalizedCart, {
enabled: input.enabled ?? Boolean(resolvedCartId)
});
const completeCart = cartFlow.useCompleteCart();
const { paymentProviders, initiatePaymentAsync } = payment;
const { mutateAsync: completeCartAsync } = completeCart;
const resolvePaymentProviderId = options?.resolvePaymentProviderId;
return useMutation({
mutationFn: async (request)=>{
const { resolvedCartId: mutationCartId, normalizedCart: mutationCart } = resolveCheckoutCartInput({
cartId: input.cartId,
cart: input.cart
});
if (!mutationCartId) throw createErrorWithStage("cart", "Cart id is required");
const effectiveCart = resolveCheckoutEffectiveCart({
cartId: mutationCartId,
cart: mutationCart,
getCachedCart: (effectiveCartId)=>getCachedCartById(queryClient, storefront.queryKeys.cart, effectiveCartId, {
isActiveCartQueryKey
}) ?? null
});
const effectiveRegionId = input.regionId ?? effectiveCart?.region_id ?? void 0;
let paymentProviderId;
try {
paymentProviderId = await resolveCheckoutPaymentProviderId({
effectiveCart,
effectiveRegionId,
requestedPaymentProviderId: request?.paymentProviderId,
paymentProviders,
resolvePaymentProviderId,
queryClient
});
} catch (error) {
throw createErrorWithStage("payment_provider", "Failed to resolve payment provider", error);
}
let paymentCollection;
try {
paymentCollection = await ensureCheckoutPaymentCollection({
effectiveCart,
paymentProviderId,
initiatePaymentAsync
});
} catch (error) {
throw createErrorWithStage("payment", "Failed to initiate payment", error);
}
let order;
try {
order = await completeCheckoutOrder({
cartId: mutationCartId,
completeCartMutation: completeCartAsync
});
} catch (error) {
throw createErrorWithStage("complete", "Failed to complete checkout", error);
}
return {
order,
paymentCollection,
paymentProviderId
};
},
onSuccess: (result)=>{
options?.onSuccess?.(result);
},
onError: (error)=>{
options?.onError?.(error);
}
});
}
return {
useCheckoutShipping,
useCheckoutPayment,
useCompleteCheckout
};
}
export { createMedusaCheckoutFlow };