diff --git a/.changeset/fix-free-pickup-validation.md b/.changeset/fix-free-pickup-validation.md
new file mode 100644
index 00000000..6a903497
--- /dev/null
+++ b/.changeset/fix-free-pickup-validation.md
@@ -0,0 +1,5 @@
+---
+"@godaddy/react": patch
+---
+
+Fix "Complete your free order" button not working for free pickup orders. Now only requires billing first and last name instead of full billing address.
diff --git a/packages/react/src/components/checkout/address/address-form.tsx b/packages/react/src/components/checkout/address/address-form.tsx
index dcdba20d..ef8e9258 100644
--- a/packages/react/src/components/checkout/address/address-form.tsx
+++ b/packages/react/src/components/checkout/address/address-form.tsx
@@ -52,7 +52,13 @@ import { eventIds } from '@/tracking/events';
import { TrackingEventType, track } from '@/tracking/track';
import type { Address } from '@/types';
-export function AddressForm({ sectionKey }: { sectionKey: string }) {
+interface AddressFormProps {
+ sectionKey: string;
+ /** When true, only show first name and last name fields (used for free pickup orders) */
+ onlyNames?: boolean;
+}
+
+export function AddressForm({ sectionKey, onlyNames = false }: AddressFormProps) {
const form = useFormContext();
const { session } = useCheckoutContext();
const { t } = useGoDaddyContext();
@@ -303,126 +309,131 @@ export function AddressForm({ sectionKey }: { sectionKey: string }) {
return (
);
}
diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx
index 5012b3ed..934f02b8 100644
--- a/packages/react/src/components/checkout/checkout.tsx
+++ b/packages/react/src/components/checkout/checkout.tsx
@@ -13,7 +13,7 @@ import { type Theme, useTheme } from '@/hooks/use-theme';
import { useVariables } from '@/hooks/use-variables';
import type { TrackingProperties } from '@/tracking/event-properties';
import { TrackingProvider } from '@/tracking/tracking-provider';
-import type { CheckoutSession } from '@/types';
+import { PaymentMethodType, type CheckoutSession } from '@/types';
import { CheckoutFormContainer } from './form/checkout-form-container';
import type { Target } from './target/target';
@@ -280,32 +280,19 @@ export function Checkout(props: CheckoutProps) {
}
// Billing address validation - only required if not using shipping address OR pickup
- const requireBillingAddress =
- !data.paymentUseShippingAddress ||
- data.deliveryMethod === DeliveryMethods.PICKUP;
-
- if (requireBillingAddress) {
- // Basic billing fields required for all countries
- const billingFields = [
+ // BUT skip for free orders (paymentMethod === 'offline')
+ const isFreeOrder = data.paymentMethod === PaymentMethodType.OFFLINE;
+ const isPickup = data.deliveryMethod === DeliveryMethods.PICKUP;
+ const isFreePickup = isFreeOrder && isPickup;
+
+ // For free pickup orders, only require first/last name (no address)
+ if (isFreePickup) {
+ const nameFields = [
{ key: 'billingFirstName', message: t.validation.enterFirstName },
{ key: 'billingLastName', message: t.validation.enterLastName },
- { key: 'billingAddressLine1', message: t.validation.enterAddress },
- { key: 'billingAdminArea2', message: t.validation.enterCity },
- {
- key: 'billingPostalCode',
- message: t.validation.enterZipPostalCode,
- },
- { key: 'billingCountryCode', message: t.validation.enterCountry },
];
- if (hasRegionData(String(data.billingCountryCode))) {
- billingFields.push({
- key: 'billingAdminArea1',
- message: t.validation.selectState,
- });
- }
-
- for (const { key, message } of billingFields) {
+ for (const { key, message } of nameFields) {
if (!data[key as keyof typeof data]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -314,6 +301,42 @@ export function Checkout(props: CheckoutProps) {
});
}
}
+ } else {
+ // Full billing address validation - required if not using shipping address OR pickup
+ const requireBillingAddress =
+ !data.paymentUseShippingAddress || isPickup;
+
+ if (requireBillingAddress) {
+ // Basic billing fields required for all countries
+ const billingFields = [
+ { key: 'billingFirstName', message: t.validation.enterFirstName },
+ { key: 'billingLastName', message: t.validation.enterLastName },
+ { key: 'billingAddressLine1', message: t.validation.enterAddress },
+ { key: 'billingAdminArea2', message: t.validation.enterCity },
+ {
+ key: 'billingPostalCode',
+ message: t.validation.enterZipPostalCode,
+ },
+ { key: 'billingCountryCode', message: t.validation.enterCountry },
+ ];
+
+ if (hasRegionData(String(data.billingCountryCode))) {
+ billingFields.push({
+ key: 'billingAdminArea1',
+ message: t.validation.selectState,
+ });
+ }
+
+ for (const { key, message } of billingFields) {
+ if (!data[key as keyof typeof data]) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message,
+ path: [key],
+ });
+ }
+ }
+ }
}
// Shipping address validation - only required if delivery method is SHIP
diff --git a/packages/react/src/components/checkout/form/custom-form-provider.tsx b/packages/react/src/components/checkout/form/custom-form-provider.tsx
index 2b9eba50..4602126c 100644
--- a/packages/react/src/components/checkout/form/custom-form-provider.tsx
+++ b/packages/react/src/components/checkout/form/custom-form-provider.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import type { FieldPath, UseFormReturn, UseFormTrigger } from 'react-hook-form';
import { FormProvider } from 'react-hook-form';
+import { PaymentMethodType } from '@/types';
import type { CheckoutFormData } from '../checkout';
import { DeliveryMethods } from '../delivery/delivery-method';
@@ -50,18 +51,28 @@ export function CustomFormProvider<
else {
const values = currentMethods.getValues();
const deliveryMethod = values.deliveryMethod as unknown as string;
+ const paymentMethod = values.paymentMethod as unknown as string;
const paymentUseShippingAddress =
values.paymentUseShippingAddress as unknown as boolean;
const isPickup = deliveryMethod === DeliveryMethods.PICKUP;
const isShipping = deliveryMethod === DeliveryMethods.SHIP;
- const requireBillingAddress = !paymentUseShippingAddress || isPickup;
+ const isFreeOrder = paymentMethod === PaymentMethodType.OFFLINE;
+ const isFreePickup = isFreeOrder && isPickup;
// Get all field names and filter based on conditions
const allFieldNames = Object.keys(values);
let fieldNames = [...allFieldNames] as Array>;
- /* If using shipping address for billing (and not pickup), filter out billing-related field validations */
- if (!requireBillingAddress) {
+ /* For free pickup orders, only validate billingFirstName and billingLastName */
+ if (isFreePickup) {
+ fieldNames = fieldNames.filter(
+ fieldName =>
+ !fieldName.startsWith('billing') ||
+ fieldName === 'billingFirstName' ||
+ fieldName === 'billingLastName'
+ );
+ } else if (paymentUseShippingAddress && !isPickup) {
+ /* If using shipping address for billing (and not pickup), filter out billing-related field validations */
fieldNames = fieldNames.filter(
fieldName => !fieldName.startsWith('billing')
);
@@ -76,7 +87,7 @@ export function CustomFormProvider<
// Trigger validation only on the filtered fields if any condition is true,
// otherwise trigger on all fields
- if (paymentUseShippingAddress || isPickup) {
+ if (paymentUseShippingAddress || isPickup || isFreeOrder) {
result = await methods.trigger(fieldNames, triggerOptions);
} else {
result = await methods.trigger(undefined, triggerOptions);
diff --git a/packages/react/src/components/checkout/payment/free-payment-form.test.ts b/packages/react/src/components/checkout/payment/free-payment-form.test.ts
new file mode 100644
index 00000000..198dbf3b
--- /dev/null
+++ b/packages/react/src/components/checkout/payment/free-payment-form.test.ts
@@ -0,0 +1,188 @@
+import { describe, expect, it } from 'vitest';
+
+/**
+ * Helper function to determine which billing fields are required
+ * based on payment method and delivery method.
+ *
+ * This mirrors the validation logic in checkout.tsx and custom-form-provider.tsx
+ */
+function getRequiredBillingFields(
+ paymentMethod: string,
+ deliveryMethod: string,
+ paymentUseShippingAddress: boolean
+): string[] {
+ const isFreeOrder = paymentMethod === 'offline';
+ const isPickup = deliveryMethod === 'PICKUP';
+ const isFreePickup = isFreeOrder && isPickup;
+
+ // For free pickup orders, only require name fields
+ if (isFreePickup) {
+ return ['billingFirstName', 'billingLastName'];
+ }
+
+ // For paid pickup or when not using shipping address, require full billing
+ const requireBillingAddress = !paymentUseShippingAddress || isPickup;
+
+ if (requireBillingAddress) {
+ return [
+ 'billingFirstName',
+ 'billingLastName',
+ 'billingAddressLine1',
+ 'billingAdminArea2',
+ 'billingPostalCode',
+ 'billingCountryCode',
+ ];
+ }
+
+ // When using shipping address for billing (non-pickup), no billing fields required
+ return [];
+}
+
+/**
+ * Helper function to filter billing field names for validation
+ * This mirrors the logic in custom-form-provider.tsx
+ */
+function filterBillingFieldsForValidation(
+ allFieldNames: string[],
+ paymentMethod: string,
+ deliveryMethod: string,
+ paymentUseShippingAddress: boolean
+): string[] {
+ const isFreeOrder = paymentMethod === 'offline';
+ const isPickup = deliveryMethod === 'PICKUP';
+ const isFreePickup = isFreeOrder && isPickup;
+
+ if (isFreePickup) {
+ // For free pickup, only include billingFirstName and billingLastName
+ return allFieldNames.filter(
+ fieldName =>
+ !fieldName.startsWith('billing') ||
+ fieldName === 'billingFirstName' ||
+ fieldName === 'billingLastName'
+ );
+ }
+
+ if (paymentUseShippingAddress && !isPickup) {
+ // Filter out all billing fields when using shipping address
+ return allFieldNames.filter(fieldName => !fieldName.startsWith('billing'));
+ }
+
+ // Return all fields for other cases
+ return allFieldNames;
+}
+
+describe('FreePaymentForm - Billing validation logic', () => {
+ describe('getRequiredBillingFields', () => {
+ it('should only require first and last name for free pickup orders', () => {
+ const result = getRequiredBillingFields('offline', 'PICKUP', true);
+
+ expect(result).toEqual(['billingFirstName', 'billingLastName']);
+ expect(result).not.toContain('billingAddressLine1');
+ expect(result).not.toContain('billingPostalCode');
+ });
+
+ it('should require full billing address for paid pickup orders', () => {
+ const result = getRequiredBillingFields('card', 'PICKUP', true);
+
+ expect(result).toContain('billingFirstName');
+ expect(result).toContain('billingLastName');
+ expect(result).toContain('billingAddressLine1');
+ expect(result).toContain('billingAdminArea2');
+ expect(result).toContain('billingPostalCode');
+ expect(result).toContain('billingCountryCode');
+ });
+
+ it('should require full billing when not using shipping address', () => {
+ const result = getRequiredBillingFields('card', 'SHIP', false);
+
+ expect(result).toContain('billingFirstName');
+ expect(result).toContain('billingAddressLine1');
+ });
+
+ it('should not require billing fields when using shipping address for shipping orders', () => {
+ const result = getRequiredBillingFields('card', 'SHIP', true);
+
+ expect(result).toEqual([]);
+ });
+
+ it('should only require names for free orders with SHIP delivery using shipping address', () => {
+ // Free order with shipping that uses shipping address - no billing needed
+ const result = getRequiredBillingFields('offline', 'SHIP', true);
+
+ // For free non-pickup orders using shipping address, no billing required
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('filterBillingFieldsForValidation', () => {
+ const allFields = [
+ 'contactEmail',
+ 'deliveryMethod',
+ 'billingFirstName',
+ 'billingLastName',
+ 'billingAddressLine1',
+ 'billingAdminArea1',
+ 'billingAdminArea2',
+ 'billingPostalCode',
+ 'billingCountryCode',
+ 'shippingFirstName',
+ 'shippingLastName',
+ 'shippingAddressLine1',
+ ];
+
+ it('should filter billing fields to only names for free pickup', () => {
+ const result = filterBillingFieldsForValidation(
+ allFields,
+ 'offline',
+ 'PICKUP',
+ true
+ );
+
+ expect(result).toContain('contactEmail');
+ expect(result).toContain('billingFirstName');
+ expect(result).toContain('billingLastName');
+ expect(result).not.toContain('billingAddressLine1');
+ expect(result).not.toContain('billingPostalCode');
+ });
+
+ it('should keep all billing fields for paid pickup', () => {
+ const result = filterBillingFieldsForValidation(
+ allFields,
+ 'card',
+ 'PICKUP',
+ true
+ );
+
+ expect(result).toContain('billingFirstName');
+ expect(result).toContain('billingAddressLine1');
+ expect(result).toContain('billingPostalCode');
+ });
+
+ it('should filter out all billing fields when using shipping address for non-pickup', () => {
+ const result = filterBillingFieldsForValidation(
+ allFields,
+ 'card',
+ 'SHIP',
+ true
+ );
+
+ expect(result).toContain('contactEmail');
+ expect(result).toContain('shippingFirstName');
+ expect(result).not.toContain('billingFirstName');
+ expect(result).not.toContain('billingAddressLine1');
+ });
+
+ it('should keep all fields when not using shipping address', () => {
+ const result = filterBillingFieldsForValidation(
+ allFields,
+ 'card',
+ 'SHIP',
+ false
+ );
+
+ expect(result).toContain('billingFirstName');
+ expect(result).toContain('billingAddressLine1');
+ expect(result).toContain('shippingFirstName');
+ });
+ });
+});
diff --git a/packages/react/src/components/checkout/payment/free-payment-form.tsx b/packages/react/src/components/checkout/payment/free-payment-form.tsx
index 399012cd..4ca61617 100644
--- a/packages/react/src/components/checkout/payment/free-payment-form.tsx
+++ b/packages/react/src/components/checkout/payment/free-payment-form.tsx
@@ -1,7 +1,9 @@
import { LoaderCircle } from 'lucide-react';
import React from 'react';
import { useFormContext } from 'react-hook-form';
+import { AddressForm } from '@/components/checkout/address/address-form';
import { useCheckoutContext } from '@/components/checkout/checkout';
+import { DeliveryMethods } from '@/components/checkout/delivery/delivery-method';
import {
PaymentProvider,
useConfirmCheckout,
@@ -20,6 +22,9 @@ export function FreePaymentForm() {
const form = useFormContext();
const confirmCheckout = useConfirmCheckout();
+ const deliveryMethod = form.watch('deliveryMethod');
+ const isPickup = deliveryMethod === DeliveryMethods.PICKUP;
+
const handleSubmit = React.useCallback(async () => {
const valid = await form.trigger();
if (!valid) {
@@ -43,7 +48,7 @@ export function FreePaymentForm() {
}
}, [form, confirmCheckout.mutateAsync, setCheckoutErrors]);
- return isConfirmingCheckout ? (
+ const submitButton = isConfirmingCheckout ? (
);
+
+ // For pickup orders, show name fields
+ if (isPickup) {
+ return (
+