Skip to content
Open
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/legal-beers-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@godaddy/react": patch
---

Add ui extension support for targets
3 changes: 2 additions & 1 deletion packages/react/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"includes": [
"**/*",
"!!**/src/globals.css",
"!!**/src/globals-tailwind.css"
"!!**/src/globals-tailwind.css",
"!!**/src/lib/godaddy/*-env.ts"
]
},
"linter": {
Expand Down
8 changes: 8 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
"./package.json": "./package.json",
"./styles.css": "./dist/index.css",
"./styles-tailwind.css": "./dist/index-tailwind.css",
"./ui-extensions": {
"types": "./dist/ui-extensions/index.d.ts",
"import": "./dist/ui-extensions/index.js",
"default": "./dist/ui-extensions/index.js"
},
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js",
Expand All @@ -28,6 +33,9 @@
"server": [
"./dist/server.d.ts"
],
"ui-extensions": [
"./dist/ui-extensions/index.d.ts"
],
"*": [
"./dist/index.d.ts"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,41 @@ describe('Checkout billing behavior', () => {
});
});

it('calculates purchase-mode taxes from the collected billing address', async () => {
const { user } = renderCheckout({
draftOrderOverrides: {
billing: { address: buildBillingAddress({ addressLine1: '' }) },
lineItems: [{ fulfillmentMode: 'PURCHASE' }],
},
sessionOverrides: {
enableShipping: false,
enableLocalPickup: false,
enableBillingAddressCollection: true,
enableTaxCollection: true,
},
});
await waitForCheckoutReady();

await typeIntoNamedField(user, 'billingFirstName', 'Bill');
await typeIntoNamedField(user, 'billingLastName', 'Buyer');
await typeIntoNamedField(user, 'billingAddressLine1', '789 Billing Rd');
await typeIntoNamedField(user, 'billingAdminArea2', 'Atlanta');
await typeIntoNamedField(user, 'billingPostalCode', '30301');
await advanceCheckoutDebounce();
await waitForOperation('CalculateCheckoutSessionTaxes');

expect(
getOperations('CalculateCheckoutSessionTaxes').at(-1)?.input
).toMatchObject({
destination: expect.objectContaining({
addressLine1: '789 Billing Rd',
adminArea2: 'Atlanta',
postalCode: '30301',
countryCode: 'US',
}),
});
});

it('copies explicit shipping patches to billing while same-as-shipping is checked, then stops after unchecked', async () => {
const draftOrder = buildDraftOrder();
const session = buildCheckoutSession({ draftOrder });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,107 @@ describe('Checkout draft-order field sync', () => {
});
});

it('syncs a complete shipping address without requiring first or last name', async () => {
const { user } = renderCheckout({
draftOrderOverrides: {
shipping: {
firstName: '',
lastName: '',
address: buildShippingAddress({
addressLine1: '',
addressLine2: '',
adminArea1: 'GA',
adminArea2: '',
postalCode: '',
countryCode: 'US',
}),
},
billing: {
firstName: '',
lastName: '',
address: null,
},
},
});
await waitForCheckoutReady();
clearOperations();

await typeIntoNamedField(user, 'shippingAddressLine1', '456 Shipping Ln');
await typeIntoNamedField(user, 'shippingAdminArea2', 'Jasper');
await typeIntoNamedField(user, 'shippingPostalCode', '30143');
await advanceCheckoutDebounce();
await waitForOperation('UpdateCheckoutSessionDraftOrder');

expect(getLastUpdateInput()).toMatchObject({
shipping: {
address: expect.objectContaining({
addressLine1: '456 Shipping Ln',
adminArea2: 'Jasper',
postalCode: '30143',
countryCode: 'US',
}),
},
billing: {
address: expect.objectContaining({
addressLine1: '456 Shipping Ln',
adminArea2: 'Jasper',
postalCode: '30143',
countryCode: 'US',
}),
},
});
expect(getLastUpdateInput()?.shipping).not.toHaveProperty('firstName');
expect(getLastUpdateInput()?.shipping).not.toHaveProperty('lastName');
expect(getLastUpdateInput()?.billing).not.toHaveProperty('firstName');
expect(getLastUpdateInput()?.billing).not.toHaveProperty('lastName');
});

it('syncs a complete billing address without requiring first or last name', async () => {
const { user } = renderCheckout({
draftOrderOverrides: {
billing: {
firstName: '',
lastName: '',
address: buildShippingAddress({
addressLine1: '',
addressLine2: '',
adminArea1: 'GA',
adminArea2: '',
postalCode: '',
countryCode: 'US',
}),
},
lineItems: [{ fulfillmentMode: DeliveryMethods.PURCHASE }],
},
sessionOverrides: {
enableShipping: false,
enableLocalPickup: false,
enableBillingAddressCollection: true,
},
});
await waitForCheckoutReady();
clearOperations();

await typeIntoNamedField(user, 'billingAddressLine1', '789 Billing Rd');
await typeIntoNamedField(user, 'billingAdminArea2', 'Atlanta');
await typeIntoNamedField(user, 'billingPostalCode', '30301');
await advanceCheckoutDebounce();
await waitForOperation('UpdateCheckoutSessionDraftOrder');

expect(getLastUpdateInput()).toMatchObject({
billing: {
address: expect.objectContaining({
addressLine1: '789 Billing Rd',
adminArea2: 'Atlanta',
postalCode: '30301',
countryCode: 'US',
}),
},
});
expect(getLastUpdateInput()?.billing).not.toHaveProperty('firstName');
expect(getLastUpdateInput()?.billing).not.toHaveProperty('lastName');
});

it('serializes slow field-by-field edits without concurrent update mutations', async () => {
const { user } = renderCheckout({
apiOverrides: { updateDraftOrderDelayMs: 200 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,67 @@ describe('Checkout pickup location and time selection', () => {
});
});

it('reapplies the selected pickup location when refetched line items need pickup fulfillment sync', async () => {
const location = buildPickupLocation({
id: 'default-location',
isDefault: true,
address: {
addressLine1: '599 Stegall Dr',
addressLine2: '',
addressLine3: '',
adminArea1: 'GA',
adminArea2: 'Jasper',
adminArea3: 'Jasper Store',
adminArea4: '',
postalCode: '30143',
countryCode: 'US',
},
});
const draftOrder = buildDraftOrder({
lineItems: [{ id: 'line-1', fulfillmentMode: 'PICKUP' }],
shippingLines: [],
});
const session = buildCheckoutSession({
draftOrder,
locations: [location],
defaultOperatingHours: location.operatingHours,
paymentMethods: offlinePaymentMethods(),
});

const { queryClient } = renderCheckout({ session, draftOrder });
await waitForCheckoutReady();
await waitForOperation('ApplyCheckoutSessionFulfillmentLocation');
clearOperations();

const refetchedDraftOrder = buildDraftOrder({
lineItems: [
{ id: 'line-1', fulfillmentMode: 'PICKUP' },
{ id: 'line-2', fulfillmentMode: 'NONE' },
],
shippingLines: [],
});

queryClient.setQueryData(checkoutQueryKeys.draftOrder(session.id), {
checkoutSession: { ...session, draftOrder: refetchedDraftOrder },
});
await flushPromises();

await waitForOperation('ApplyCheckoutSessionFulfillmentLocation');
await waitForOperation('CalculateCheckoutSessionTaxes');

expect(
getOperations('ApplyCheckoutSessionFulfillmentLocation')
).toHaveLength(1);
expect(
getOperations('ApplyCheckoutSessionFulfillmentLocation').at(-1)?.input
).toMatchObject({ fulfillmentLocationId: 'default-location' });
expect(
getOperations('CalculateCheckoutSessionTaxes').at(-1)?.input
).toMatchObject({
destination: expect.objectContaining({ postalCode: '30143' }),
});
});

it('preserves the default pickup location across refetches before confirming', async () => {
const location = buildPickupLocation({
id: 'default-location',
Expand Down
45 changes: 28 additions & 17 deletions packages/react/src/components/checkout/address/address-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,28 @@ export function AddressForm({

const shouldUpdateAddress = Boolean(
addressHasChanged && // Only sync if address values differ from order
!!firstName?.trim() &&
!!lastName?.trim() &&
isAddressComplete(address) &&
debouncedSectionContactAndAddress ===
serializedSectionContactAndAddress &&
!isAutocompleteOpen
);

const hasCompleteName = Boolean(firstName?.trim() && lastName?.trim());
const addressSyncFieldNames = React.useMemo(
() => [
...(hasCompleteName
? [`${sectionKey}FirstName`, `${sectionKey}LastName`]
: []),
`${sectionKey}AddressLine1`,
`${sectionKey}AddressLine2`,
`${sectionKey}AdminArea2`,
`${sectionKey}AdminArea1`,
`${sectionKey}PostalCode`,
`${sectionKey}CountryCode`,
],
[hasCompleteName, sectionKey]
);

useDraftOrderFieldSync({
key: 'address',
data: sectionContactAndAddress,
Expand All @@ -353,23 +367,20 @@ export function AddressForm({
debouncedSectionContactAndAddress,
],
enabled: !onlyNames && shouldUpdateAddress,
fieldNames: [
`${sectionKey}FirstName`,
`${sectionKey}LastName`,
`${sectionKey}AddressLine1`,
`${sectionKey}AddressLine2`,
`${sectionKey}AdminArea2`,
`${sectionKey}AdminArea1`,
`${sectionKey}PostalCode`,
`${sectionKey}CountryCode`,
],
fieldNames: addressSyncFieldNames,
mapToInput: data => {
const fields = {
...(hasCompleteName
? {
firstName: data.firstName.trim(),
lastName: data.lastName.trim(),
}
: {}),
address: data.address,
};

return mapAddressFieldsToInput(
{
firstName: data.firstName.trim(),
lastName: data.lastName.trim(),
address: data.address,
},
fields,
sectionKey as 'shipping' | 'billing',
useShippingAddress
);
Expand Down
7 changes: 4 additions & 3 deletions packages/react/src/components/checkout/checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { TrackingProperties } from '@/tracking/event-properties';
import { TrackingProvider } from '@/tracking/tracking-provider';
import { type CheckoutSession, PaymentMethodType } from '@/types';
import { CheckoutFormContainer } from './form/checkout-form-container';
import type { Target } from './target/target';
import type { Target } from './target/types';

// Utility function for redirecting to success URL after checkout
export function redirectToSuccessUrl(successUrl?: string): void {
Expand Down Expand Up @@ -48,7 +48,8 @@ export type LayoutSection =
| 'payment'
| 'pickup'
| 'tips'
| 'delivery';
| 'delivery'
| 'notes';

export const LayoutSections = {
EXPRESS_CHECKOUT: 'express-checkout',
Expand All @@ -58,6 +59,7 @@ export const LayoutSections = {
PICKUP: 'pickup',
DELIVERY: 'delivery',
TIPS: 'tips',
NOTES: 'notes',
} as const;

export type StripeConfig = {
Expand Down Expand Up @@ -247,7 +249,6 @@ export function Checkout(props: CheckoutProps) {
const { t } = useGoDaddyContext();

const { session, jwt, isLoading: isLoadingJWT } = useCheckoutSession(props);

useTheme(session?.appearance?.theme);
useVariables(session?.appearance?.variables || props?.appearance?.variables);

Expand Down
Loading