Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sync-shipping-fulfillment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/react": patch
---

Re-apply shipping methods when checkout line items change after shipping has already been selected.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
useCheckoutContext,
} from '@/components/checkout/checkout';
import { DeliveryMethods } from '@/components/checkout/delivery/delivery-method';
import { useDraftOrder } from '@/components/checkout/order/use-draft-order';
import { buildPickupPayload } from '@/components/checkout/pickup/utils/build-pickup-payload';
import { getShippingFulfillmentSyncKey } from '@/components/checkout/shipping/utils/should-apply-shipping-method';
import { useGoDaddyContext } from '@/godaddy-provider';
import { confirmCheckout } from '@/lib/godaddy/godaddy';
import { eventIds } from '@/tracking/events';
Expand Down Expand Up @@ -72,6 +74,7 @@ export function useConfirmCheckout() {
useCheckoutContext();
const { apiHost } = useGoDaddyContext();
const form = useFormContext();
const { data: order } = useDraftOrder();
const isPendingRef = useRef(false);

return useMutation({
Expand All @@ -87,9 +90,22 @@ export function useConfirmCheckout() {

const { isExpress, ...confirmCheckoutInput } = input;

const isPickup =
form.getValues('deliveryMethod') === DeliveryMethods.PICKUP &&
!isExpress;
const deliveryMethod = form.getValues('deliveryMethod');
const isPickup = deliveryMethod === DeliveryMethods.PICKUP && !isExpress;
const isShipping = deliveryMethod === DeliveryMethods.SHIP && !isExpress;

const hasShippingLines = (order?.shippingLines?.length ?? 0) > 0;
const hasLineItemsMissingShippingFulfillment = Boolean(
getShippingFulfillmentSyncKey(order?.lineItems)
);

if (
isShipping &&
(!hasShippingLines || hasLineItemsMissingShippingFulfillment)
) {
setCheckoutErrors(['SHIPPING_METHOD_APPLICATION_FAILED']);
throw new Error('SHIPPING_METHOD_APPLICATION_FAILED');
}

const pickUpData = isPickup
? buildPickupPayload({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { useFormContext } from 'react-hook-form';
import { useCheckoutContext } from '@/components/checkout/checkout';
Expand All @@ -12,6 +13,10 @@ import { useUpdateTaxes } from '@/components/checkout/order/use-update-taxes';
import { useIsPaymentDisabled } from '@/components/checkout/payment/utils/use-is-payment-disabled';
import { ShippingMethodSkeleton } from '@/components/checkout/shipping/shipping-method-skeleton';
import { filterAndSortShippingMethods } from '@/components/checkout/shipping/utils/filter-shipping-methods';
import {
getShippingFulfillmentSyncKey,
shouldApplyShippingMethod,
} from '@/components/checkout/shipping/utils/should-apply-shipping-method';
import { useApplyShippingMethod } from '@/components/checkout/shipping/utils/use-apply-shipping-method';
import { useDraftOrderShippingMethods } from '@/components/checkout/shipping/utils/use-draft-order-shipping-methods';
import { useFormatCurrency } from '@/components/checkout/utils/format-currency';
Expand Down Expand Up @@ -45,8 +50,10 @@ export function ShippingMethodForm() {
const formatCurrency = useFormatCurrency();
const form = useFormContext();
const { t } = useGoDaddyContext();
const { session, isConfirmingCheckout } = useCheckoutContext();
const { session, isConfirmingCheckout, setCheckoutErrors } =
useCheckoutContext();
const updateTaxes = useUpdateTaxes();
const queryClient = useQueryClient();
const isPaymentDisabled = useIsPaymentDisabled();

const { data: shippingMethodsData, isLoading: isShippingMethodsLoading } =
Expand All @@ -63,6 +70,8 @@ export function ShippingMethodForm() {
lineItem => lineItem.fulfillmentMode === DeliveryMethods.PICKUP
)
);
const fulfillmentSyncKey = getShippingFulfillmentSyncKey(order?.lineItems);
const hasLineItemsMissingShippingFulfillment = Boolean(fulfillmentSyncKey);

const orderSubTotal = totals?.subTotal?.value || 0;

Expand All @@ -81,23 +90,39 @@ export function ShippingMethodForm() {
hadShippingMethods: boolean;
wasPickup: boolean;
clearedShippingMethod: boolean;
blockedFulfillmentKey: string | null;
}>({
serviceCode: null,
cost: null,
hadShippingMethods: false,
wasPickup: false,
clearedShippingMethod: false,
blockedFulfillmentKey: null,
});

useEffect(() => {
if (isShippingMethodsLoading || isDraftOrderLoading || isConfirmingCheckout)
if (
isShippingMethodsLoading ||
isDraftOrderLoading ||
isConfirmingCheckout ||
applyShippingMethod.isPending
)
return;

const hasShippingMethods = (shippingMethods?.length ?? 0) > 0;
const currentServiceCode = shippingLines?.requestedService || null;
const currentCost = shippingLines?.amount?.value ?? null;
const lastState = lastProcessedStateRef.current;

if (
!hasLineItemsMissingShippingFulfillment &&
lastState.blockedFulfillmentKey
) {
lastProcessedStateRef.current = {
...lastState,
blockedFulfillmentKey: null,
};
}

// Case 1: No shipping methods available - clear shipping and set fulfillment to SHIP
if (!hasShippingMethods && hasShippingAddress) {
// Apply empty shipping method if:
Expand All @@ -116,6 +141,7 @@ export function ShippingMethodForm() {
hadShippingMethods: false,
wasPickup: isPickup,
clearedShippingMethod: true,
blockedFulfillmentKey: null,
};
}
return;
Expand All @@ -141,25 +167,53 @@ export function ShippingMethodForm() {
: null;

const methodToApply = matchedMethod || firstMethod;
const methodCost = methodToApply.cost?.value ?? null;

// Check if we've already processed this exact state
const alreadyProcessed =
methodToApply.serviceCode === lastState.serviceCode &&
methodCost === lastState.cost;
// Check if we've already processed this exact state. If cart contents
// changed after a shipping method was selected, shippingLines can still
// match the selected rate while new line items are NONE. In that case we
// must re-apply the method so checkout-api recalculates shipping and sets
// line item fulfillmentMode to SHIP.
const { alreadyProcessed, needsMutation, methodCost } =
shouldApplyShippingMethod({
methodToApply,
shippingLine: shippingLines,
lastState,
fulfillmentSyncKey,
});

if (!alreadyProcessed) {
form.setValue('shippingMethod', methodToApply.serviceCode, {
shouldDirty: false,
});

// Only mutate if the method or cost actually changed on the order
const needsMutation =
methodToApply.serviceCode !== currentServiceCode ||
methodCost !== currentCost;

if (needsMutation) {
applyShippingMethod.mutate(buildShippingPayload(methodToApply));
const isFulfillmentSync = Boolean(
hasLineItemsMissingShippingFulfillment && fulfillmentSyncKey
);

if (isFulfillmentSync) {
lastProcessedStateRef.current = {
...lastProcessedStateRef.current,
blockedFulfillmentKey: fulfillmentSyncKey,
};
}

applyShippingMethod.mutate(buildShippingPayload(methodToApply), {
onSuccess: () => {
if (!isFulfillmentSync || !session?.id) return;

queryClient.invalidateQueries({
queryKey: ['draft-order', session.id],
});
},
onError: () => {
if (!isFulfillmentSync || !session?.id) return;

setCheckoutErrors(['SHIPPING_METHOD_APPLICATION_FAILED']);
queryClient.invalidateQueries({
queryKey: ['draft-order', session.id],
});
},
});
} else if (session?.enableTaxCollection) {
updateTaxes.mutate(undefined);
}
Expand All @@ -170,6 +224,9 @@ export function ShippingMethodForm() {
hadShippingMethods: true,
wasPickup: false,
clearedShippingMethod: false,
blockedFulfillmentKey: needsMutation
? lastProcessedStateRef.current.blockedFulfillmentKey
: null,
};
}
}
Expand All @@ -182,9 +239,15 @@ export function ShippingMethodForm() {
form,
applyShippingMethod,
updateTaxes.mutate,
setCheckoutErrors,
session?.enableTaxCollection,
queryClient,
session?.id,
isPickup,
isDraftOrderLoading,
hasLineItemsMissingShippingFulfillment,
applyShippingMethod.isPending,
order?.lineItems,
]);

if (isShippingMethodsLoading || isShippingAddressLoading) {
Expand Down
Loading