Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
13 changes: 13 additions & 0 deletions packages/core/src/app/checkout/CheckoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,17 @@ const Checkout = ({
[navigateToNextIncompleteStep, navigateToStep],
);

// themeV2 embeds the billing form in the payment step, so the same-as-shipping
// toggle lives there instead of on shipping. Keep the checkout-wide value in
// sync so returning to shipping doesn't re-copy shipping over a billing address
// the shopper set on payment. Unlike handleShippingNextStep this never navigates.
const handleBillingSameAsShippingChange = useCallback(
(isBillingSameAsShipping: boolean): void => {
setState((prev) => ({ ...prev, isBillingSameAsShipping }));
},
[],
);

const handleShippingSignIn = useCallback((): void => {
setCustomerViewType(CustomerViewType.Login);
}, [setCustomerViewType]);
Expand Down Expand Up @@ -533,12 +544,14 @@ const Checkout = ({
checkEmbeddedSupport={checkEmbeddedSupport}
consignments={consignments}
errorLogger={errorLogger}
isBillingSameAsShipping={isBillingSameAsShipping}
isEmbedded={isEmbedded()}
isUsingMultiShipping={
cart && consignments
? isUsingMultiShipping(consignments, cart.lineItems)
: false
}
onBillingSameAsShippingChange={handleBillingSameAsShippingChange}
onCartChangedError={handleCartChangedError}
onEdit={handleEditStep}
onExpanded={handleExpanded}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/app/checkout/components/PaymentStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const PaymentStep = ({
capabilities,
consignments,
errorLogger,
isBillingSameAsShipping,
onBillingSameAsShippingChange,
onEdit,
onExpanded,
checkEmbeddedSupport,
Expand All @@ -60,12 +62,14 @@ const PaymentStep = ({
capabilities={capabilities}
checkEmbeddedSupport={checkEmbeddedSupport}
errorLogger={errorLogger}
isBillingSameAsShipping={isBillingSameAsShipping}
isEmbedded={isEmbedded()}
isUsingMultiShipping={
cart && consignments
? isUsingMultiShipping(consignments, cart.lineItems)
: false
}
onBillingSameAsShippingChange={onBillingSameAsShippingChange}
onCartChangedError={onCartChangedError}
onFinalize={onFinalize}
onReady={onReady}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/app/payment/Payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ import { getFilteredPaymentMethodsWithDefault } from './paymentMethodFilters';
export interface PaymentProps {
capabilities: Capabilities;
errorLogger: ErrorLogger;
isBillingSameAsShipping?: boolean;
isEmbedded?: boolean;
isUsingMultiShipping?: boolean;
checkEmbeddedSupport?(methodIds: string[]): void; // TODO: We're currently doing this check in multiple places, perhaps we should move it up so this check get be done in a single place instead.
onBillingSameAsShippingChange?(isBillingSameAsShipping: boolean): void;
onCartChangedError?(): void;
onFinalize?(): void;
onFinalizeError?(error: Error): void;
Expand Down Expand Up @@ -783,13 +785,15 @@ const Payment = (
defaultMethodId={props.defaultMethod?.id || ''}
didExceedSpamLimit={state.didExceedSpamLimit}
disableStoreCredit={disableStoreCredit}
isBillingSameAsShipping={props.isBillingSameAsShipping}
isEmbedded={props.isEmbedded}
isInitializingPayment={props.isInitializingPayment}
isPaymentDataRequired={props.isPaymentDataRequired}
isStoreCreditApplied={props.isStoreCreditApplied}
isTermsConditionsRequired={props.isTermsConditionsRequired}
isUsingMultiShipping={props.isUsingMultiShipping}
methods={props.methods}
onBillingSameAsShippingChange={props.onBillingSameAsShippingChange}
onMethodSelect={setSelectedMethod}
onStoreCreditChange={handleStoreCreditChange}
onSubmit={handleSubmit}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/app/payment/PaymentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface PaymentFormProps {
defaultGatewayId?: string;
defaultMethodId: string;
didExceedSpamLimit?: boolean;
isBillingSameAsShipping?: boolean;
isEmbedded?: boolean;
isInitializingPayment?: boolean;
isTermsConditionsRequired?: boolean;
Expand All @@ -68,6 +69,7 @@ export interface PaymentFormProps {
usableStoreCredit?: number;
validationSchema?: ObjectSchema<Partial<PaymentFormValues>>;
isPaymentDataRequired(): boolean;
onBillingSameAsShippingChange?(isBillingSameAsShipping: boolean): void;
onMethodSelect?(method: PaymentMethod): void;
onStoreCreditChange?(useStoreCredit?: boolean): void;
onSubmit?(values: PaymentFormValues): void;
Expand All @@ -81,6 +83,7 @@ const PaymentForm: FunctionComponent<
availableStoreCredit = 0,
disableStoreCredit = false,
didExceedSpamLimit,
isBillingSameAsShipping,
isEmbedded,
isInitializingPayment,
isPaymentDataRequired,
Expand All @@ -89,6 +92,7 @@ const PaymentForm: FunctionComponent<
isUsingMultiShipping,
language,
methods,
onBillingSameAsShippingChange,
onMethodSelect,
onStoreCreditChange,
onUnhandledError,
Expand Down Expand Up @@ -202,7 +206,9 @@ const PaymentForm: FunctionComponent<

{themeV2 && (
<PaymentBillingBlock
isBillingSameAsShipping={isBillingSameAsShipping ?? true}
methodId={selectedMethod?.id}
onBillingSameAsShippingChange={onBillingSameAsShippingChange ?? noop}
onUnhandledError={onUnhandledError ?? noop}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type CheckoutService,
createCheckoutService,
} from '@bigcommerce/checkout-sdk';
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import React, { type FunctionComponent } from 'react';

import {
Expand All @@ -23,6 +23,7 @@ import { type BillingFormValues } from '../../billing/billingFormConfig';
import { getCheckout } from '../../checkout/checkouts.mock';
import { getStoreConfig } from '../../config/config.mock';
import { getCustomer } from '../../customer/customers.mock';
import { getShippingAddress } from '../../shipping/shipping-addresses.mock';

import { PaymentBillingBlock } from './PaymentBillingBlock';
import { type PaymentBillingFormProps } from './PaymentBillingForm';
Expand Down Expand Up @@ -80,19 +81,31 @@ describe('PaymentBillingBlock', () => {
jest.spyOn(checkoutState.data, 'getBillingAddressFields').mockReturnValue(formFields);
jest.spyOn(checkoutState.data, 'getAddressExtraFields').mockReturnValue([]);
jest.spyOn(checkoutState.data, 'getBillingAddress').mockReturnValue(undefined);

PaymentBillingBlockTest = ({ methodId }) => (
<CheckoutProvider checkoutService={checkoutService}>
<LocaleContext.Provider value={localeContext}>
<CapabilitiesContext.Provider value={defaultCapabilities}>
<PaymentBillingBlock
methodId={methodId}
onUnhandledError={onUnhandledError}
/>
</CapabilitiesContext.Provider>
</LocaleContext.Provider>
</CheckoutProvider>
);
jest.spyOn(checkoutState.data, 'getShippingAddress').mockReturnValue(getShippingAddress());

// "Billing same as shipping" is now checkout-wide state owned by
// CheckoutPage. Stand in for that here so the block reflects the value and
// reports the shopper's choice back up, matching the real wiring.
PaymentBillingBlockTest = ({ methodId }) => {
const [isBillingSameAsShipping, setIsBillingSameAsShipping] = React.useState(
getStoreConfig().checkoutSettings.checkoutBillingSameAsShippingEnabled ?? true,
);

return (
<CheckoutProvider checkoutService={checkoutService}>
<LocaleContext.Provider value={localeContext}>
<CapabilitiesContext.Provider value={defaultCapabilities}>
<PaymentBillingBlock
isBillingSameAsShipping={isBillingSameAsShipping}
methodId={methodId}
onBillingSameAsShippingChange={setIsBillingSameAsShipping}
onUnhandledError={onUnhandledError}
/>
</CapabilitiesContext.Provider>
</LocaleContext.Provider>
</CheckoutProvider>
);
};
});

it('renders the billing address heading', async () => {
Expand Down Expand Up @@ -124,6 +137,56 @@ describe('PaymentBillingBlock', () => {
expect(mockCapturedProps.methodId).toBe('amazonpay');
});

it('defaults the same-as-shipping toggle from checkout settings', async () => {
render(<PaymentBillingBlockTest />);

await screen.findByTestId('trigger-persist');

expect(mockCapturedProps.isBillingSameAsShipping).toBe(true);
});

it('copies the shipping address to billing when the toggle is checked', async () => {
render(<PaymentBillingBlockTest />);

await screen.findByTestId('trigger-persist');

act(() => {
mockCapturedProps.onBillingSameAsShippingChange(true);
});

await new Promise((resolve) => process.nextTick(resolve));

expect(checkoutService.updateBillingAddress).toHaveBeenCalledWith(getShippingAddress());
});

it('does not copy shipping to billing when the toggle is unchecked', async () => {
render(<PaymentBillingBlockTest />);

await screen.findByTestId('trigger-persist');

act(() => {
mockCapturedProps.onBillingSameAsShippingChange(false);
});

await new Promise((resolve) => process.nextTick(resolve));

expect(checkoutService.updateBillingAddress).not.toHaveBeenCalled();
});

it('remembers the shopper unchecking the toggle across re-renders', async () => {
render(<PaymentBillingBlockTest />);

await screen.findByTestId('trigger-persist');

expect(mockCapturedProps.isBillingSameAsShipping).toBe(true);

act(() => {
mockCapturedProps.onBillingSameAsShippingChange(false);
});

await waitFor(() => expect(mockCapturedProps.isBillingSameAsShipping).toBe(false));
});

it('shows a warning instead of the form when manual entry is restricted and there are no saved addresses', async () => {
jest.spyOn(checkoutState.data, 'getCustomer').mockReturnValue({
...getCustomer(),
Expand All @@ -142,7 +205,11 @@ describe('PaymentBillingBlock', () => {
},
}}
>
<PaymentBillingBlock onUnhandledError={onUnhandledError} />
<PaymentBillingBlock
isBillingSameAsShipping={true}
onBillingSameAsShippingChange={jest.fn()}
onUnhandledError={onUnhandledError}
/>
</CapabilitiesContext.Provider>
</LocaleContext.Provider>
</CheckoutProvider>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface PaymentBillingBlockProps {
// billing form's method-specific behaviour (e.g. Amazon Pay's static address
// + reduced schema). Must reflect the live selection, not checkout.payments.
methodId?: string;
isBillingSameAsShipping: boolean;
onBillingSameAsShippingChange(isBillingSameAsShipping: boolean): void;
onUnhandledError(error: Error): void;
}

Expand All @@ -42,6 +44,8 @@ const getFieldsWithExtraFields = (

export const PaymentBillingBlock: FunctionComponent<PaymentBillingBlockProps> = ({
methodId,
isBillingSameAsShipping,
onBillingSameAsShippingChange,
onUnhandledError,
}) => {
const {
Expand All @@ -53,9 +57,10 @@ export const PaymentBillingBlock: FunctionComponent<PaymentBillingBlockProps> =
getBillingAddressFields,
getAddressExtraFields,
},
// getBillingAddress guarantees the latest state inside async callbacks
// getBillingAddress / getShippingAddress guarantee the latest state inside
// async callbacks
checkoutState: {
data: { getBillingAddress },
data: { getBillingAddress, getShippingAddress },
},
checkoutService,
} = useCheckout(({ data, statuses }) => ({
Expand Down Expand Up @@ -84,6 +89,24 @@ export const PaymentBillingBlock: FunctionComponent<PaymentBillingBlockProps> =
const customerMessage = checkout.customerMessage;
const billingAddress = getBillingAddress();

const handleBillingSameAsShippingChange = (checked: boolean) => {
onBillingSameAsShippingChange(checked);

if (!checked) {
return;
}

const shippingAddress = getShippingAddress();

if (shippingAddress && !isEqualAddress(shippingAddress, getBillingAddress())) {
checkoutService.updateBillingAddress(shippingAddress).catch((error) => {
if (error instanceof Error) {
onUnhandledError(error);
}
});
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
};
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

const getFields = useCallback(
(countryCode?: string) =>
getFieldsWithExtraFields(
Expand Down Expand Up @@ -163,8 +186,10 @@ export const PaymentBillingBlock: FunctionComponent<PaymentBillingBlockProps> =
billingAddress={billingAddress}
customerMessage={customerMessage}
getFields={getFields}
isBillingSameAsShipping={isBillingSameAsShipping}
isLoading={isInitializing}
methodId={methodId}
onBillingSameAsShippingChange={handleBillingSameAsShippingChange}
onPersist={handlePersist}
onUnhandledError={onUnhandledError}
/>
Expand Down
Loading