Customer Data Store and revised handling for Shipping As Billing (https://github.com/woocommerce/woocommerce-blocks/pull/5817)
* Add address-related items to wc/store/cart data store * include shippingAsBilling in return value of useCustomerData * Add useUpdateCustomerData hook This allows us to have a single hook responsible for updating the customer information on the server. * Add useUpdateCustomerData hook in Checkout block * Remove the updating customer data work from the useCustomerData hook * Remove shippingAsBilling from previousCustomerData ref type * Add useShippingAsBillingCheckbox hook * Control shippingAsBilling from single hook * Remove checkbox handling from useCheckoutAddress * Remove CustomerDataContext typedef * Merge with woocommerce/woocommerce-blocks#5810 changes * Move shipping as billing to checkout state context provider * Unused import * Subscribe to changes * Only receiveCartContents when updating customer data via checkout * Cache customerDataToUpdate * rename debounced function * Combine customerDataType and customerDataContextType * Change case of CustomerDataType * debouncedUpdateCustomerData typo * Fix notice context * Clean up inline docs for push changes * Comment on dirty state * Phone is always set * shippingAddress is never undefined * setBillingPhone * receiveCartContents explanation * Tweak customerData to avoid null * useShippingAsBilling Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
This commit is contained in:
parent
a02e79ea38
commit
0714fa41bd
|
@ -83,26 +83,26 @@ describe( 'AddressForm Component', () => {
|
|||
const WrappedAddressForm = ( { type } ) => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
setShippingFields,
|
||||
shippingFields,
|
||||
setShippingAddress,
|
||||
shippingAddress,
|
||||
} = useCheckoutAddress();
|
||||
|
||||
return (
|
||||
<AddressForm
|
||||
type={ type }
|
||||
onChange={ setShippingFields }
|
||||
values={ shippingFields }
|
||||
onChange={ setShippingAddress }
|
||||
values={ shippingAddress }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
const ShippingFields = () => {
|
||||
const { shippingFields } = useCheckoutAddress();
|
||||
const { shippingAddress } = useCheckoutAddress();
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{ Object.keys( shippingFields ).map( ( key ) => (
|
||||
<li key={ key }>{ key + ': ' + shippingFields[ key ] }</li>
|
||||
{ Object.keys( shippingAddress ).map( ( key ) => (
|
||||
<li key={ key }>{ key + ': ' + shippingAddress[ key ] }</li>
|
||||
) ) }
|
||||
</ul>
|
||||
);
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { defaultAddressFields } from '@woocommerce/settings';
|
||||
import { useEffect, useCallback, useRef } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
useShippingDataContext,
|
||||
useCustomerDataContext,
|
||||
} from '../providers/cart-checkout';
|
||||
|
||||
/**
|
||||
* Custom hook for exposing address related functionality for the checkout address form.
|
||||
*/
|
||||
export const useCheckoutAddress = () => {
|
||||
const { needsShipping } = useShippingDataContext();
|
||||
const {
|
||||
billingData,
|
||||
setBillingData,
|
||||
shippingAddress,
|
||||
setShippingAddress,
|
||||
shippingAsBilling,
|
||||
setShippingAsBilling,
|
||||
} = useCustomerDataContext();
|
||||
|
||||
const currentShippingAsBilling = useRef( shippingAsBilling );
|
||||
const previousBillingData = useRef();
|
||||
|
||||
/**
|
||||
* Sets shipping address data, and also billing if using the same address.
|
||||
*/
|
||||
const setShippingFields = useCallback(
|
||||
( value ) => {
|
||||
setShippingAddress( value );
|
||||
|
||||
if ( shippingAsBilling ) {
|
||||
setBillingData( value );
|
||||
}
|
||||
},
|
||||
[ shippingAsBilling, setShippingAddress, setBillingData ]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets billing address data, and also shipping if shipping is disabled.
|
||||
*/
|
||||
const setBillingFields = useCallback(
|
||||
( value ) => {
|
||||
setBillingData( value );
|
||||
|
||||
if ( ! needsShipping ) {
|
||||
setShippingAddress( value );
|
||||
}
|
||||
},
|
||||
[ needsShipping, setShippingAddress, setBillingData ]
|
||||
);
|
||||
|
||||
// When the "Use same address" checkbox is toggled we need to update the current billing address to reflect this.
|
||||
// This either sets the billing address to the shipping address, or restores the billing address to it's previous state.
|
||||
useEffect( () => {
|
||||
if ( currentShippingAsBilling.current !== shippingAsBilling ) {
|
||||
if ( shippingAsBilling ) {
|
||||
previousBillingData.current = billingData;
|
||||
setBillingData( shippingAddress );
|
||||
} else {
|
||||
const {
|
||||
// We need to pluck out email from previous billing data because they can be empty, causing the current email to get emptied. See issue #4155
|
||||
/* eslint-disable no-unused-vars */
|
||||
email,
|
||||
/* eslint-enable no-unused-vars */
|
||||
...billingAddress
|
||||
} = previousBillingData.current || billingData;
|
||||
|
||||
setBillingData( {
|
||||
...billingAddress,
|
||||
} );
|
||||
}
|
||||
currentShippingAsBilling.current = shippingAsBilling;
|
||||
}
|
||||
}, [ shippingAsBilling, setBillingData, shippingAddress, billingData ] );
|
||||
|
||||
const setEmail = useCallback(
|
||||
( value ) =>
|
||||
void setBillingData( {
|
||||
email: value,
|
||||
} ),
|
||||
[ setBillingData ]
|
||||
);
|
||||
|
||||
const setPhone = useCallback(
|
||||
( value ) =>
|
||||
void setBillingData( {
|
||||
phone: value,
|
||||
} ),
|
||||
[ setBillingData ]
|
||||
);
|
||||
|
||||
const setShippingPhone = useCallback(
|
||||
( value ) =>
|
||||
void setShippingFields( {
|
||||
phone: value,
|
||||
} ),
|
||||
[ setShippingFields ]
|
||||
);
|
||||
|
||||
// Note that currentShippingAsBilling is returned rather than the current state of shippingAsBilling--this is so that
|
||||
// the billing fields are not rendered before sync (billing field values are debounced and would be outdated)
|
||||
return {
|
||||
defaultAddressFields,
|
||||
shippingFields: shippingAddress,
|
||||
setShippingFields,
|
||||
billingFields: billingData,
|
||||
setBillingFields,
|
||||
setEmail,
|
||||
setPhone,
|
||||
setShippingPhone,
|
||||
shippingAsBilling,
|
||||
setShippingAsBilling,
|
||||
showShippingFields: needsShipping,
|
||||
showBillingFields:
|
||||
! needsShipping || ! currentShippingAsBilling.current,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
defaultAddressFields,
|
||||
AddressFields,
|
||||
EnteredAddress,
|
||||
ShippingAddress,
|
||||
BillingAddress,
|
||||
} from '@woocommerce/settings';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
useShippingDataContext,
|
||||
useCheckoutContext,
|
||||
} from '../providers/cart-checkout';
|
||||
import { useCustomerData } from './use-customer-data';
|
||||
|
||||
interface CheckoutAddress {
|
||||
shippingAddress: ShippingAddress;
|
||||
billingData: BillingAddress;
|
||||
setShippingAddress: ( data: Partial< EnteredAddress > ) => void;
|
||||
setBillingData: ( data: Partial< EnteredAddress > ) => void;
|
||||
setEmail: ( value: string ) => void;
|
||||
setBillingPhone: ( value: string ) => void;
|
||||
setShippingPhone: ( value: string ) => void;
|
||||
useShippingAsBilling: boolean;
|
||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
|
||||
defaultAddressFields: AddressFields;
|
||||
showShippingFields: boolean;
|
||||
showBillingFields: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for exposing address related functionality for the checkout address form.
|
||||
*/
|
||||
export const useCheckoutAddress = (): CheckoutAddress => {
|
||||
const { needsShipping } = useShippingDataContext();
|
||||
const {
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling,
|
||||
} = useCheckoutContext();
|
||||
const {
|
||||
billingData,
|
||||
setBillingData,
|
||||
shippingAddress,
|
||||
setShippingAddress,
|
||||
} = useCustomerData();
|
||||
|
||||
const setEmail = useCallback(
|
||||
( value ) =>
|
||||
void setBillingData( {
|
||||
email: value,
|
||||
} ),
|
||||
[ setBillingData ]
|
||||
);
|
||||
|
||||
const setBillingPhone = useCallback(
|
||||
( value ) =>
|
||||
void setBillingData( {
|
||||
phone: value,
|
||||
} ),
|
||||
[ setBillingData ]
|
||||
);
|
||||
|
||||
const setShippingPhone = useCallback(
|
||||
( value ) =>
|
||||
void setShippingAddress( {
|
||||
phone: value,
|
||||
} ),
|
||||
[ setShippingAddress ]
|
||||
);
|
||||
|
||||
return {
|
||||
shippingAddress,
|
||||
billingData,
|
||||
setShippingAddress,
|
||||
setBillingData,
|
||||
setEmail,
|
||||
setBillingPhone,
|
||||
setShippingPhone,
|
||||
defaultAddressFields,
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling,
|
||||
showShippingFields: needsShipping,
|
||||
showBillingFields: ! needsShipping || ! useShippingAsBilling,
|
||||
};
|
||||
};
|
|
@ -1,225 +1,33 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useEffect, useState, useCallback, useRef } from '@wordpress/element';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
import {
|
||||
formatStoreApiErrorMessage,
|
||||
pluckAddress,
|
||||
pluckEmail,
|
||||
} from '@woocommerce/base-utils';
|
||||
import {
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingAddress,
|
||||
BillingAddressShippingAddress,
|
||||
} from '@woocommerce/types';
|
||||
import type { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
||||
|
||||
declare type CustomerData = {
|
||||
billingData: CartResponseBillingAddress;
|
||||
shippingAddress: CartResponseShippingAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreCart } from './cart/use-store-cart';
|
||||
import { useStoreNotices } from './use-store-notices';
|
||||
|
||||
function instanceOfCartResponseBillingAddress(
|
||||
address: CartResponseBillingAddress | CartResponseShippingAddress
|
||||
): address is CartResponseBillingAddress {
|
||||
return 'email' in address;
|
||||
export interface CustomerDataType {
|
||||
isInitialized: boolean;
|
||||
billingData: BillingAddress;
|
||||
shippingAddress: ShippingAddress;
|
||||
setBillingData: ( data: Partial< BillingAddress > ) => void;
|
||||
setShippingAddress: ( data: Partial< ShippingAddress > ) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a shallow compare of important address data to determine if the cart needs updating on the server.
|
||||
*
|
||||
* This takes the current and previous address into account, as well as the billing email field.
|
||||
*
|
||||
* @param {Object} previousAddress An object containing all previous address information
|
||||
* @param {Object} address An object containing all address information
|
||||
*
|
||||
* @return {boolean} True if the store needs updating due to changed data.
|
||||
*/
|
||||
const shouldUpdateAddressStore = <
|
||||
T extends CartResponseBillingAddress | CartResponseShippingAddress
|
||||
>(
|
||||
previousAddress: T,
|
||||
address: T
|
||||
): boolean => {
|
||||
if (
|
||||
instanceOfCartResponseBillingAddress( address ) &&
|
||||
pluckEmail( address ) !==
|
||||
pluckEmail( previousAddress as CartResponseBillingAddress )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!! address.country &&
|
||||
! isShallowEqual(
|
||||
pluckAddress( previousAddress ),
|
||||
pluckAddress( address )
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a custom hook for syncing customer address data (billing and shipping) with the server.
|
||||
*/
|
||||
export const useCustomerData = (): {
|
||||
billingData: CartResponseBillingAddress;
|
||||
shippingAddress: CartResponseShippingAddress;
|
||||
setBillingData: ( data: CartResponseBillingAddress ) => void;
|
||||
setShippingAddress: ( data: CartResponseShippingAddress ) => void;
|
||||
} => {
|
||||
const { updateCustomerData } = useDispatch( storeKey );
|
||||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
||||
|
||||
// Grab the initial values from the store cart hook.
|
||||
// NOTE: The initial values may not be current if the cart has not yet finished loading. See cartIsLoading.
|
||||
const {
|
||||
billingAddress: initialBillingAddress,
|
||||
shippingAddress: initialShippingAddress,
|
||||
cartIsLoading,
|
||||
} = useStoreCart();
|
||||
|
||||
// We only want to update the local state once, otherwise the data on the checkout page gets overwritten
|
||||
// with the initial state of the addresses. We also only want to start triggering updates to the server when the
|
||||
// initial data has fully initialized. Track that header.
|
||||
const [ isInitialized, setIsInitialized ] = useState< boolean >( false );
|
||||
|
||||
// State of customer data is tracked here from this point, using the initial values from the useStoreCart hook.
|
||||
const [ customerData, setCustomerData ] = useState< CustomerData >( {
|
||||
billingData: initialBillingAddress,
|
||||
shippingAddress: initialShippingAddress,
|
||||
} );
|
||||
|
||||
// Store values last sent to the server in a ref to avoid requests unless important fields are changed.
|
||||
const previousCustomerData = useRef< CustomerData >( customerData );
|
||||
|
||||
// When the cart data is resolved from server for the first time (using cartIsLoading) we need to update
|
||||
// the initial billing and shipping values to respect customer data from the server.
|
||||
useEffect( () => {
|
||||
if ( isInitialized || cartIsLoading ) {
|
||||
return;
|
||||
}
|
||||
const initializedCustomerData = {
|
||||
billingData: initialBillingAddress,
|
||||
shippingAddress: initialShippingAddress,
|
||||
export const useCustomerData = (): CustomerDataType => {
|
||||
const { customerData, isInitialized } = useSelect( ( select ) => {
|
||||
const store = select( storeKey );
|
||||
return {
|
||||
customerData: store.getCustomerData(),
|
||||
isInitialized: store.hasFinishedResolution( 'getCartData' ),
|
||||
};
|
||||
// Updates local state to the now-resolved cart address.
|
||||
previousCustomerData.current = initializedCustomerData;
|
||||
setCustomerData( initializedCustomerData );
|
||||
// We are now initialized.
|
||||
setIsInitialized( true );
|
||||
}, [
|
||||
cartIsLoading,
|
||||
isInitialized,
|
||||
initialBillingAddress,
|
||||
initialShippingAddress,
|
||||
] );
|
||||
|
||||
/**
|
||||
* Set billing data.
|
||||
*
|
||||
* Callback used to set billing data for the customer. This merges the previous and new state, and in turn,
|
||||
* will trigger an update to the server if enough data has changed (see the useEffect call below).
|
||||
*
|
||||
* This callback contains special handling for the "email" address field so that field is never overwritten if
|
||||
* simply updating the billing address and not the email address.
|
||||
*/
|
||||
const setBillingData = useCallback( ( newData ) => {
|
||||
setCustomerData( ( prevState ) => {
|
||||
return {
|
||||
...prevState,
|
||||
billingData: {
|
||||
...prevState.billingData,
|
||||
...newData,
|
||||
},
|
||||
};
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
/**
|
||||
* Set shipping address.
|
||||
*
|
||||
* Callback used to set shipping data for the customer. This merges the previous and new state, and in turn, will
|
||||
* trigger an update to the server if enough data has changed (see the useEffect call below).
|
||||
*/
|
||||
const setShippingAddress = useCallback( ( newData ) => {
|
||||
setCustomerData( ( prevState ) => {
|
||||
return {
|
||||
...prevState,
|
||||
shippingAddress: {
|
||||
...prevState.shippingAddress,
|
||||
...newData,
|
||||
},
|
||||
};
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
/**
|
||||
* This pushes changes to the API when the local state differs from the address in the cart.
|
||||
*
|
||||
* The function shouldUpdateAddressStore() determines if enough data has changed to trigger the update.
|
||||
*/
|
||||
const pushCustomerData = () => {
|
||||
const customerDataToUpdate: Partial< BillingAddressShippingAddress > = {};
|
||||
|
||||
if (
|
||||
shouldUpdateAddressStore(
|
||||
previousCustomerData.current.billingData,
|
||||
customerData.billingData
|
||||
)
|
||||
) {
|
||||
customerDataToUpdate.billing_address = customerData.billingData;
|
||||
}
|
||||
|
||||
if (
|
||||
shouldUpdateAddressStore(
|
||||
previousCustomerData.current.shippingAddress,
|
||||
customerData.shippingAddress
|
||||
)
|
||||
) {
|
||||
customerDataToUpdate.shipping_address =
|
||||
customerData.shippingAddress;
|
||||
}
|
||||
|
||||
if ( Object.keys( customerDataToUpdate ).length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousCustomerData.current = customerData;
|
||||
|
||||
updateCustomerData( customerDataToUpdate )
|
||||
.then( () => {
|
||||
removeNotice( 'checkout' );
|
||||
} )
|
||||
.catch( ( response ) => {
|
||||
addErrorNotice( formatStoreApiErrorMessage( response ), {
|
||||
id: 'checkout',
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
const debouncedPushCustomerData = useDebouncedCallback(
|
||||
pushCustomerData,
|
||||
1000
|
||||
);
|
||||
|
||||
// If data changes, trigger an update to the server only if initialized.
|
||||
useEffect( () => {
|
||||
if ( ! isInitialized ) {
|
||||
return;
|
||||
}
|
||||
debouncedPushCustomerData();
|
||||
}, [ isInitialized, customerData, debouncedPushCustomerData ] );
|
||||
} );
|
||||
const { setShippingAddress, setBillingData } = useDispatch( storeKey );
|
||||
|
||||
return {
|
||||
isInitialized,
|
||||
billingData: customerData.billingData,
|
||||
shippingAddress: customerData.shippingAddress,
|
||||
setBillingData,
|
||||
|
|
|
@ -19,6 +19,7 @@ export enum ACTION {
|
|||
SET_ORDER_NOTES = 'set_checkout_order_notes',
|
||||
INCREMENT_CALCULATING = 'increment_calculating',
|
||||
DECREMENT_CALCULATING = 'decrement_calculating',
|
||||
SET_SHIPPING_ADDRESS_AS_BILLING_ADDRESS = 'set_shipping_address_as_billing_address',
|
||||
SET_SHOULD_CREATE_ACCOUNT = 'set_should_create_account',
|
||||
SET_EXTENSION_DATA = 'set_extension_data',
|
||||
}
|
||||
|
@ -92,6 +93,11 @@ export const actions = {
|
|||
type: ACTION.SET_ORDER_ID,
|
||||
orderId,
|
||||
} as const ),
|
||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) =>
|
||||
( {
|
||||
type: ACTION.SET_SHIPPING_ADDRESS_AS_BILLING_ADDRESS,
|
||||
useShippingAsBilling,
|
||||
} as const ),
|
||||
setShouldCreateAccount: ( shouldCreateAccount: boolean ) =>
|
||||
( {
|
||||
type: ACTION.SET_SHOULD_CREATE_ACCOUNT,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { getSetting, EnteredAddress } from '@woocommerce/settings';
|
||||
import { isSameAddress } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -34,6 +35,8 @@ const preloadedCheckoutData = getSetting( 'checkoutData', {} ) as Record<
|
|||
const checkoutData = {
|
||||
order_id: 0,
|
||||
customer_id: 0,
|
||||
billing_address: {} as EnteredAddress,
|
||||
shipping_address: {} as EnteredAddress,
|
||||
...( preloadedCheckoutData || {} ),
|
||||
};
|
||||
|
||||
|
@ -68,6 +71,8 @@ export const DEFAULT_CHECKOUT_STATE_DATA: CheckoutStateContextType = {
|
|||
onCheckoutValidationBeforeProcessing: () => () => void null,
|
||||
hasOrder: false,
|
||||
isCart: false,
|
||||
useShippingAsBilling: false,
|
||||
setUseShippingAsBilling: ( value ) => void value,
|
||||
shouldCreateAccount: false,
|
||||
setShouldCreateAccount: ( value ) => void value,
|
||||
extensionData: {},
|
||||
|
@ -81,6 +86,10 @@ export const DEFAULT_STATE: CheckoutStateContextState = {
|
|||
orderId: checkoutData.order_id,
|
||||
orderNotes: '',
|
||||
customerId: checkoutData.customer_id,
|
||||
useShippingAsBilling: isSameAddress(
|
||||
checkoutData.billing_address,
|
||||
checkoutData.shipping_address
|
||||
),
|
||||
shouldCreateAccount: false,
|
||||
processingResponse: null,
|
||||
extensionData: {},
|
||||
|
|
|
@ -381,6 +381,9 @@ export const CheckoutStateProvider = ( {
|
|||
hasOrder: !! checkoutState.orderId,
|
||||
customerId: checkoutState.customerId,
|
||||
orderNotes: checkoutState.orderNotes,
|
||||
useShippingAsBilling: checkoutState.useShippingAsBilling,
|
||||
setUseShippingAsBilling: ( value ) =>
|
||||
dispatch( actions.setUseShippingAsBilling( value ) ),
|
||||
shouldCreateAccount: checkoutState.shouldCreateAccount,
|
||||
setShouldCreateAccount: ( value ) =>
|
||||
dispatch( actions.setShouldCreateAccount( value ) ),
|
||||
|
|
|
@ -17,6 +17,7 @@ export const reducer = (
|
|||
orderId,
|
||||
orderNotes,
|
||||
extensionData,
|
||||
useShippingAsBilling,
|
||||
shouldCreateAccount,
|
||||
data,
|
||||
}: ActionType
|
||||
|
@ -152,6 +153,17 @@ export const reducer = (
|
|||
}
|
||||
: state;
|
||||
break;
|
||||
case ACTION.SET_SHIPPING_ADDRESS_AS_BILLING_ADDRESS:
|
||||
if (
|
||||
useShippingAsBilling !== undefined &&
|
||||
useShippingAsBilling !== state.useShippingAsBilling
|
||||
) {
|
||||
newState = {
|
||||
...state,
|
||||
useShippingAsBilling,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case ACTION.SET_SHOULD_CREATE_ACCOUNT:
|
||||
if (
|
||||
shouldCreateAccount !== undefined &&
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface CheckoutStateContextState {
|
|||
orderId: number;
|
||||
orderNotes: string;
|
||||
customerId: number;
|
||||
useShippingAsBilling: boolean;
|
||||
shouldCreateAccount: boolean;
|
||||
processingResponse: PaymentResultDataType | null;
|
||||
extensionData: extensionData;
|
||||
|
@ -88,6 +89,8 @@ export type CheckoutStateContextType = {
|
|||
onCheckoutBeforeProcessing: ReturnType< typeof emitterCallback >;
|
||||
// Used to register a callback that will fire when the checkout has been submitted before being sent off to the server.
|
||||
onCheckoutValidationBeforeProcessing: ReturnType< typeof emitterCallback >;
|
||||
// Toggle using shipping address as billing address.
|
||||
setUseShippingAsBilling: ( useShippingAsBilling: boolean ) => void;
|
||||
// Set if user account should be created.
|
||||
setShouldCreateAccount: ( shouldCreateAccount: boolean ) => void;
|
||||
// True when the checkout has a draft order from the API.
|
||||
|
@ -104,6 +107,8 @@ export type CheckoutStateContextType = {
|
|||
orderNotes: CheckoutStateContextState[ 'orderNotes' ];
|
||||
// This is the ID of the customer the draft order belongs to.
|
||||
customerId: CheckoutStateContextState[ 'customerId' ];
|
||||
// Should the billing form be hidden and inherit the shipping address?
|
||||
useShippingAsBilling: CheckoutStateContextState[ 'useShippingAsBilling' ];
|
||||
// Should a user account be created?
|
||||
shouldCreateAccount: CheckoutStateContextState[ 'shouldCreateAccount' ];
|
||||
// Custom checkout data passed to the store API on processing.
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { CustomerDataType } from '../../../hooks/use-customer-data';
|
||||
|
||||
export const defaultBillingData: CustomerDataType[ 'billingData' ] = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
};
|
||||
|
||||
export const defaultShippingAddress: CustomerDataType[ 'shippingAddress' ] = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
phone: '',
|
||||
};
|
|
@ -1,122 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createContext, useContext, useState } from '@wordpress/element';
|
||||
import { defaultAddressFields } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCustomerData } from '../../../hooks/use-customer-data';
|
||||
import { useCheckoutContext } from '../checkout-state';
|
||||
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').CustomerDataContext} CustomerDataContext
|
||||
* @typedef {import('@woocommerce/type-defs/billing').BillingData} BillingData
|
||||
* @typedef {import('@woocommerce/type-defs/shipping').ShippingAddress} ShippingAddress
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {BillingData}
|
||||
*/
|
||||
const defaultBillingData = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {ShippingAddress}
|
||||
*/
|
||||
export const defaultShippingAddress = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
phone: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates CustomerDataContext
|
||||
*/
|
||||
const CustomerDataContext = createContext( {
|
||||
billingData: defaultBillingData,
|
||||
shippingAddress: defaultShippingAddress,
|
||||
setBillingData: () => null,
|
||||
setShippingAddress: () => null,
|
||||
shippingAsBilling: true,
|
||||
setShippingAsBilling: () => null,
|
||||
} );
|
||||
|
||||
/**
|
||||
* @return {CustomerDataContext} Returns data and functions related to customer billing and shipping addresses.
|
||||
*/
|
||||
export const useCustomerDataContext = () => {
|
||||
return useContext( CustomerDataContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two addresses and see if they are the same.
|
||||
*
|
||||
* @param {Object} address1 First address.
|
||||
* @param {Object} address2 Second address.
|
||||
*/
|
||||
const isSameAddress = ( address1, address2 ) => {
|
||||
return Object.keys( defaultAddressFields ).every(
|
||||
( field ) => address1[ field ] === address2[ field ]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Customer Data context provider.
|
||||
*
|
||||
* @param {Object} props Incoming props for the provider.
|
||||
* @param {Object} props.children The children being wrapped.
|
||||
*/
|
||||
export const CustomerDataProvider = ( { children } ) => {
|
||||
const {
|
||||
billingData,
|
||||
shippingAddress,
|
||||
setBillingData,
|
||||
setShippingAddress,
|
||||
} = useCustomerData();
|
||||
const { cartNeedsShipping: needsShipping } = useStoreCart();
|
||||
const { customerId } = useCheckoutContext();
|
||||
const [ shippingAsBilling, setShippingAsBilling ] = useState(
|
||||
() =>
|
||||
needsShipping &&
|
||||
( ! customerId || isSameAddress( shippingAddress, billingData ) )
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {CustomerDataContext}
|
||||
*/
|
||||
const contextValue = {
|
||||
billingData,
|
||||
shippingAddress,
|
||||
setBillingData,
|
||||
setShippingAddress,
|
||||
shippingAsBilling,
|
||||
setShippingAsBilling,
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomerDataContext.Provider value={ contextValue }>
|
||||
{ children }
|
||||
</CustomerDataContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createContext, useContext } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultBillingData, defaultShippingAddress } from './constants';
|
||||
import {
|
||||
useCustomerData,
|
||||
CustomerDataType,
|
||||
} from '../../../hooks/use-customer-data';
|
||||
|
||||
const CustomerDataContext = createContext< CustomerDataType >( {
|
||||
isInitialized: false,
|
||||
billingData: defaultBillingData,
|
||||
shippingAddress: defaultShippingAddress,
|
||||
setBillingData: () => void 0,
|
||||
setShippingAddress: () => void 0,
|
||||
} );
|
||||
|
||||
export const useCustomerDataContext = (): CustomerDataType => {
|
||||
return useContext( CustomerDataContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Customer Data context provider.
|
||||
*/
|
||||
export const CustomerDataProvider = ( {
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
} ): JSX.Element => {
|
||||
const contextValue = useCustomerData();
|
||||
|
||||
return (
|
||||
<CustomerDataContext.Provider value={ contextValue }>
|
||||
{ children }
|
||||
</CustomerDataContext.Provider>
|
||||
);
|
||||
};
|
|
@ -1,13 +1,27 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { defaultAddressFields } from '@woocommerce/settings';
|
||||
import prepareAddressFields from '@woocommerce/base-components/cart-checkout/address-form/prepare-address-fields';
|
||||
import { isEmail } from '@wordpress/url';
|
||||
import type {
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingAddress,
|
||||
} from '@woocommerce/types';
|
||||
import { defaultAddressFields, EnteredAddress } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Compare two addresses and see if they are the same.
|
||||
*/
|
||||
export const isSameAddress = (
|
||||
address1: EnteredAddress,
|
||||
address2: EnteredAddress
|
||||
): boolean => {
|
||||
return Object.keys( defaultAddressFields ).every(
|
||||
( field: string ) =>
|
||||
address1[ field as keyof EnteredAddress ] ===
|
||||
address2[ field as keyof EnteredAddress ]
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* pluckAddress takes a full address object and returns relevant fields for calculating
|
||||
|
|
|
@ -9,6 +9,11 @@ import {
|
|||
} from '@woocommerce/base-context';
|
||||
import { AddressForm } from '@woocommerce/base-components/cart-checkout';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
import type {
|
||||
BillingAddress,
|
||||
AddressField,
|
||||
AddressFields,
|
||||
} from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -30,9 +35,9 @@ const Block = ( {
|
|||
} ): JSX.Element => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
billingFields,
|
||||
setBillingFields,
|
||||
setPhone,
|
||||
billingData,
|
||||
setBillingData,
|
||||
setBillingPhone,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const { isEditor } = useEditorContext();
|
||||
|
@ -40,9 +45,9 @@ const Block = ( {
|
|||
// Clears data if fields are hidden.
|
||||
useEffect( () => {
|
||||
if ( ! showPhoneField ) {
|
||||
setPhone( '' );
|
||||
setBillingPhone( '' );
|
||||
}
|
||||
}, [ showPhoneField, setPhone ] );
|
||||
}, [ showPhoneField, setBillingPhone ] );
|
||||
|
||||
const addressFieldsConfig = useMemo( () => {
|
||||
return {
|
||||
|
@ -54,7 +59,11 @@ const Block = ( {
|
|||
hidden: ! showApartmentField,
|
||||
},
|
||||
};
|
||||
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
|
||||
}, [
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
|
||||
|
@ -63,20 +72,24 @@ const Block = ( {
|
|||
<AddressForm
|
||||
id="billing"
|
||||
type="billing"
|
||||
onChange={ ( values: Record< string, unknown > ) => {
|
||||
setBillingFields( values );
|
||||
onChange={ ( values: Partial< BillingAddress > ) => {
|
||||
setBillingData( values );
|
||||
dispatchCheckoutEvent( 'set-billing-address' );
|
||||
} }
|
||||
values={ billingFields }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
values={ billingData }
|
||||
fields={
|
||||
Object.keys(
|
||||
defaultAddressFields
|
||||
) as ( keyof AddressFields )[]
|
||||
}
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<PhoneNumber
|
||||
isRequired={ requirePhoneField }
|
||||
value={ billingFields.phone }
|
||||
value={ billingData.phone }
|
||||
onChange={ ( value ) => {
|
||||
setPhone( value );
|
||||
setBillingPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
step: 'billing',
|
||||
} );
|
||||
|
|
|
@ -28,7 +28,6 @@ const FrontendBlock = ( {
|
|||
className?: string;
|
||||
} ): JSX.Element | null => {
|
||||
const { isProcessing: checkoutIsProcessing } = useCheckoutContext();
|
||||
const { showBillingFields } = useCheckoutAddress();
|
||||
const {
|
||||
requireCompanyField,
|
||||
requirePhoneField,
|
||||
|
@ -36,6 +35,7 @@ const FrontendBlock = ( {
|
|||
showCompanyField,
|
||||
showPhoneField,
|
||||
} = useCheckoutBlockContext();
|
||||
const { showBillingFields } = useCheckoutAddress();
|
||||
|
||||
if ( ! showBillingFields ) {
|
||||
return null;
|
||||
|
|
|
@ -25,7 +25,7 @@ const Block = ( {
|
|||
shouldCreateAccount,
|
||||
setShouldCreateAccount,
|
||||
} = useCheckoutContext();
|
||||
const { billingFields, setEmail } = useCheckoutAddress();
|
||||
const { billingData, setEmail } = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
const onChangeEmail = ( value ) => {
|
||||
|
@ -54,7 +54,7 @@ const Block = ( {
|
|||
id="email"
|
||||
type="email"
|
||||
label={ __( 'Email address', 'woo-gutenberg-products-block' ) }
|
||||
value={ billingFields.email }
|
||||
value={ billingData.email }
|
||||
autoComplete="email"
|
||||
onChange={ onChangeEmail }
|
||||
required={ true }
|
||||
|
|
|
@ -11,6 +11,12 @@ import {
|
|||
} from '@woocommerce/base-context';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
import Noninteractive from '@woocommerce/base-components/noninteractive';
|
||||
import type {
|
||||
BillingAddress,
|
||||
ShippingAddress,
|
||||
AddressField,
|
||||
AddressFields,
|
||||
} from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -32,11 +38,12 @@ const Block = ( {
|
|||
} ): JSX.Element => {
|
||||
const {
|
||||
defaultAddressFields,
|
||||
setShippingFields,
|
||||
shippingFields,
|
||||
setShippingAsBilling,
|
||||
shippingAsBilling,
|
||||
setShippingAddress,
|
||||
setBillingData,
|
||||
shippingAddress,
|
||||
setShippingPhone,
|
||||
useShippingAsBilling,
|
||||
setUseShippingAsBilling,
|
||||
} = useCheckoutAddress();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const { isEditor } = useEditorContext();
|
||||
|
@ -58,7 +65,11 @@ const Block = ( {
|
|||
hidden: ! showApartmentField,
|
||||
},
|
||||
};
|
||||
}, [ showCompanyField, requireCompanyField, showApartmentField ] );
|
||||
}, [
|
||||
showCompanyField,
|
||||
requireCompanyField,
|
||||
showApartmentField,
|
||||
] ) as Record< keyof AddressFields, Partial< AddressField > >;
|
||||
|
||||
const AddressFormWrapperComponent = isEditor ? Noninteractive : Fragment;
|
||||
|
||||
|
@ -68,19 +79,26 @@ const Block = ( {
|
|||
<AddressForm
|
||||
id="shipping"
|
||||
type="shipping"
|
||||
onChange={ ( values: Record< string, unknown > ) => {
|
||||
setShippingFields( values );
|
||||
onChange={ ( values: Partial< ShippingAddress > ) => {
|
||||
setShippingAddress( values );
|
||||
if ( useShippingAsBilling ) {
|
||||
setBillingData( values );
|
||||
}
|
||||
dispatchCheckoutEvent( 'set-shipping-address' );
|
||||
} }
|
||||
values={ shippingFields }
|
||||
fields={ Object.keys( defaultAddressFields ) }
|
||||
values={ shippingAddress }
|
||||
fields={
|
||||
Object.keys(
|
||||
defaultAddressFields
|
||||
) as ( keyof AddressFields )[]
|
||||
}
|
||||
fieldConfig={ addressFieldsConfig }
|
||||
/>
|
||||
{ showPhoneField && (
|
||||
<PhoneNumber
|
||||
id="shipping-phone"
|
||||
isRequired={ requirePhoneField }
|
||||
value={ shippingFields.phone }
|
||||
value={ shippingAddress.phone }
|
||||
onChange={ ( value ) => {
|
||||
setShippingPhone( value );
|
||||
dispatchCheckoutEvent( 'set-phone-number', {
|
||||
|
@ -96,10 +114,13 @@ const Block = ( {
|
|||
'Use same address for billing',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ shippingAsBilling }
|
||||
onChange={ ( checked: boolean ) =>
|
||||
setShippingAsBilling( checked )
|
||||
}
|
||||
checked={ useShippingAsBilling }
|
||||
onChange={ ( checked: boolean ) => {
|
||||
setUseShippingAsBilling( checked );
|
||||
if ( checked ) {
|
||||
setBillingData( shippingAddress as BillingAddress );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -9,6 +9,8 @@ export const ACTION_TYPES = {
|
|||
SET_IS_CART_DATA_STALE: 'SET_IS_CART_DATA_STALE',
|
||||
RECEIVE_REMOVED_ITEM: 'RECEIVE_REMOVED_ITEM',
|
||||
UPDATING_CUSTOMER_DATA: 'UPDATING_CUSTOMER_DATA',
|
||||
SET_BILLING_DATA: 'SET_BILLING_DATA',
|
||||
SET_SHIPPING_ADDRESS: 'SET_SHIPPING_ADDRESS',
|
||||
UPDATING_SELECTED_SHIPPING_RATE: 'UPDATING_SELECTED_SHIPPING_RATE',
|
||||
UPDATE_LEGACY_CART_FRAGMENTS: 'UPDATE_LEGACY_CART_FRAGMENTS',
|
||||
TRIGGER_ADDING_TO_CART_EVENT: 'TRIGGER_ADDING_TO_CART_EVENT',
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
} from '@woocommerce/types';
|
||||
import { camelCase, mapKeys } from 'lodash';
|
||||
import type { AddToCartEventDetail } from '@woocommerce/type-defs/events';
|
||||
import { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -41,6 +42,29 @@ export const receiveCart = (
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an action object used in updating the store with the provided cart.
|
||||
*
|
||||
* This omits the customer addresses so that only updates to cart items and totals are received. This is useful when
|
||||
* currently editing address information to prevent it being overwritten from the server.
|
||||
*
|
||||
* This is a generic response action.
|
||||
*
|
||||
* @param {CartResponse} response
|
||||
*/
|
||||
export const receiveCartContents = (
|
||||
response: CartResponse
|
||||
): { type: string; response: Partial< Cart > } => {
|
||||
const cart = ( mapKeys( response, ( _, key ) =>
|
||||
camelCase( key )
|
||||
) as unknown ) as Cart;
|
||||
const { shippingAddress, billingAddress, ...cartWithoutAddress } = cart;
|
||||
return {
|
||||
type: types.RECEIVE_CART,
|
||||
response: cartWithoutAddress,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an action object used for receiving customer facing errors from the API.
|
||||
*
|
||||
|
@ -461,6 +485,19 @@ export function* selectShippingRate(
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets billing data locally, as opposed to updateCustomerData which sends it to the server.
|
||||
*/
|
||||
export const setBillingData = ( billingData: Partial< BillingAddress > ) =>
|
||||
( { type: types.SET_BILLING_DATA, billingData } as const );
|
||||
|
||||
/**
|
||||
* Sets shipping address locally, as opposed to updateCustomerData which sends it to the server.
|
||||
*/
|
||||
export const setShippingAddress = (
|
||||
shippingAddress: Partial< ShippingAddress >
|
||||
) => ( { type: types.SET_SHIPPING_ADDRESS, shippingAddress } as const );
|
||||
|
||||
/**
|
||||
* Updates the shipping and/or billing address for the customer and returns an
|
||||
* updated cart.
|
||||
|
@ -481,7 +518,7 @@ export function* updateCustomerData(
|
|||
cache: 'no-store',
|
||||
} );
|
||||
|
||||
yield receiveCart( response );
|
||||
yield receiveCartContents( response );
|
||||
} catch ( error ) {
|
||||
yield receiveError( error );
|
||||
yield updatingCustomerData( false );
|
||||
|
@ -501,6 +538,9 @@ export function* updateCustomerData(
|
|||
|
||||
export type CartAction = ReturnOrGeneratorYieldUnion<
|
||||
| typeof receiveCart
|
||||
| typeof receiveCartContents
|
||||
| typeof setBillingData
|
||||
| typeof setShippingAddress
|
||||
| typeof receiveError
|
||||
| typeof receiveApplyingCoupon
|
||||
| typeof receiveRemovingCoupon
|
||||
|
|
|
@ -15,8 +15,9 @@ import reducer, { State } from './reducers';
|
|||
import { controls as sharedControls } from '../shared-controls';
|
||||
import { controls } from './controls';
|
||||
import type { SelectFromMap, DispatchFromMap } from '../mapped-types';
|
||||
import { pushChanges } from './push-changes';
|
||||
|
||||
registerStore< State >( STORE_KEY, {
|
||||
const registeredStore = registerStore< State >( STORE_KEY, {
|
||||
reducer,
|
||||
actions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -25,6 +26,8 @@ registerStore< State >( STORE_KEY, {
|
|||
resolvers,
|
||||
} );
|
||||
|
||||
registeredStore.subscribe( pushChanges );
|
||||
|
||||
export const CART_STORE_KEY = STORE_KEY;
|
||||
|
||||
declare module '@wordpress/data' {
|
||||
|
@ -33,5 +36,7 @@ declare module '@wordpress/data' {
|
|||
): DispatchFromMap< typeof actions >;
|
||||
function select(
|
||||
key: typeof CART_STORE_KEY
|
||||
): SelectFromMap< typeof selectors >;
|
||||
): SelectFromMap< typeof selectors > & {
|
||||
hasFinishedResolution: ( selector: string ) => boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { debounce } from 'lodash';
|
||||
import { select, dispatch } from '@wordpress/data';
|
||||
import {
|
||||
formatStoreApiErrorMessage,
|
||||
pluckAddress,
|
||||
pluckEmail,
|
||||
} from '@woocommerce/base-utils';
|
||||
import {
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingAddress,
|
||||
} from '@woocommerce/type-defs/cart-response';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
import { BillingAddressShippingAddress } from '@woocommerce/type-defs/cart';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './constants';
|
||||
|
||||
declare type CustomerData = {
|
||||
billingData: CartResponseBillingAddress;
|
||||
shippingAddress: CartResponseShippingAddress;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a cart response contains an email property.
|
||||
*/
|
||||
const isCartResponseBillingAddress = (
|
||||
address: CartResponseBillingAddress | CartResponseShippingAddress
|
||||
): address is CartResponseBillingAddress => {
|
||||
return 'email' in address;
|
||||
};
|
||||
|
||||
/**
|
||||
* Does a shallow compare of important address data to determine if the cart needs updating on the server. This takes
|
||||
* the current and previous address into account, as well as the billing email field.
|
||||
*/
|
||||
const isAddressDirty = <
|
||||
T extends CartResponseBillingAddress | CartResponseShippingAddress
|
||||
>(
|
||||
// An object containing all previous address information
|
||||
previousAddress: T,
|
||||
// An object containing all address information.
|
||||
address: T
|
||||
): boolean => {
|
||||
if (
|
||||
isCartResponseBillingAddress( address ) &&
|
||||
pluckEmail( address ) !==
|
||||
pluckEmail( previousAddress as CartResponseBillingAddress )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
!! address.country &&
|
||||
! isShallowEqual(
|
||||
pluckAddress( previousAddress ),
|
||||
pluckAddress( address )
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Local cache of customerData used for comparisons.
|
||||
*/
|
||||
let customerData = <CustomerData>{
|
||||
billingData: {},
|
||||
shippingAddress: {},
|
||||
};
|
||||
// Tracks if customerData has been populated.
|
||||
let customerDataIsInitialized = false;
|
||||
|
||||
/**
|
||||
* Tracks which props have changed so the correct data gets pushed to the server.
|
||||
*/
|
||||
const dirtyProps = {
|
||||
billingData: false,
|
||||
shippingAddress: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to dispatch an update to the server. This is debounced.
|
||||
*/
|
||||
const updateCustomerData = debounce( (): void => {
|
||||
const { billingData, shippingAddress } = customerData;
|
||||
const customerDataToUpdate = {} as Partial< BillingAddressShippingAddress >;
|
||||
|
||||
if ( dirtyProps.billingData ) {
|
||||
customerDataToUpdate.billing_address = billingData;
|
||||
dirtyProps.billingData = false;
|
||||
}
|
||||
|
||||
if ( dirtyProps.shippingAddress ) {
|
||||
customerDataToUpdate.shipping_address = shippingAddress;
|
||||
dirtyProps.shippingAddress = false;
|
||||
}
|
||||
|
||||
if ( Object.keys( customerDataToUpdate ).length ) {
|
||||
dispatch( STORE_KEY )
|
||||
.updateCustomerData( customerDataToUpdate )
|
||||
.then( () => {
|
||||
dispatch( 'core/notices' ).removeNotice(
|
||||
'checkout',
|
||||
'wc/checkout'
|
||||
);
|
||||
} )
|
||||
.catch( ( response ) => {
|
||||
dispatch( 'core/notices' ).createNotice(
|
||||
'error',
|
||||
formatStoreApiErrorMessage( response ),
|
||||
{
|
||||
id: 'checkout',
|
||||
context: 'wc/checkout',
|
||||
}
|
||||
);
|
||||
} );
|
||||
}
|
||||
}, 1000 );
|
||||
|
||||
/**
|
||||
* After cart has fully initialized, pushes changes to the server when data in the store is changed. Updates to the
|
||||
* server are debounced to prevent excessive requests.
|
||||
*/
|
||||
export const pushChanges = (): void => {
|
||||
const store = select( STORE_KEY );
|
||||
const isInitialized = store.hasFinishedResolution( 'getCartData' );
|
||||
|
||||
if ( ! isInitialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCustomerData = store.getCustomerData();
|
||||
|
||||
if ( ! customerDataIsInitialized ) {
|
||||
customerData = newCustomerData;
|
||||
customerDataIsInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// An address is dirty and needs pushing to the server if the email, country, state, city, or postcode have changed.
|
||||
if (
|
||||
isAddressDirty( customerData.billingData, newCustomerData.billingData )
|
||||
) {
|
||||
dirtyProps.billingData = true;
|
||||
}
|
||||
|
||||
if (
|
||||
isAddressDirty(
|
||||
customerData.shippingAddress,
|
||||
newCustomerData.shippingAddress
|
||||
)
|
||||
) {
|
||||
dirtyProps.shippingAddress = true;
|
||||
}
|
||||
|
||||
customerData = newCustomerData;
|
||||
|
||||
if ( dirtyProps.billingData || dirtyProps.shippingAddress ) {
|
||||
updateCustomerData();
|
||||
}
|
||||
};
|
|
@ -69,7 +69,10 @@ const reducer: Reducer< CartState > = (
|
|||
state = {
|
||||
...state,
|
||||
errors: EMPTY_CART_ERRORS,
|
||||
cartData: action.response,
|
||||
cartData: {
|
||||
...state.cartData,
|
||||
...action.response,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
@ -84,6 +87,31 @@ const reducer: Reducer< CartState > = (
|
|||
};
|
||||
}
|
||||
break;
|
||||
case types.SET_BILLING_DATA:
|
||||
state = {
|
||||
...state,
|
||||
cartData: {
|
||||
...state.cartData,
|
||||
billingAddress: {
|
||||
...state.cartData.billingAddress,
|
||||
...action.billingData,
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
case types.SET_SHIPPING_ADDRESS:
|
||||
state = {
|
||||
...state,
|
||||
cartData: {
|
||||
...state.cartData,
|
||||
shippingAddress: {
|
||||
...state.cartData.shippingAddress,
|
||||
...action.shippingAddress,
|
||||
},
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case types.REMOVING_COUPON:
|
||||
if ( action.couponCode || action.couponCode === '' ) {
|
||||
state = {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import type { Cart, CartTotals, CartMeta, CartItem } from '@woocommerce/types';
|
||||
import { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -19,6 +20,18 @@ export const getCartData = ( state: CartState ): Cart => {
|
|||
return state.cartData;
|
||||
};
|
||||
|
||||
export const getCustomerData = (
|
||||
state: CartState
|
||||
): {
|
||||
shippingAddress: ShippingAddress;
|
||||
billingData: BillingAddress;
|
||||
} => {
|
||||
return {
|
||||
shippingAddress: state.cartData.shippingAddress,
|
||||
billingData: state.cartData.billingAddress,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves cart totals from state.
|
||||
*
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface EnteredAddress {
|
|||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export type KeyedAddressField = AddressField & {
|
||||
|
@ -54,7 +55,9 @@ export type KeyedAddressField = AddressField & {
|
|||
errorMessage?: string;
|
||||
};
|
||||
export type ShippingAddress = EnteredAddress;
|
||||
export type BillingAddress = EnteredAddress;
|
||||
export interface BillingAddress extends EnteredAddress {
|
||||
email: string;
|
||||
}
|
||||
export type CountryAddressFields = Record< string, AddressFields >;
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,17 +8,6 @@
|
|||
* @typedef {import('./add-to-cart-form').AddToCartFormEventRegistration} AddToCartFormEventRegistration
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CustomerDataContext
|
||||
*
|
||||
* @property {BillingData} billingData The current billing data, including address and email.
|
||||
* @property {CartShippingAddress} shippingAddress The current set address for shipping.
|
||||
* @property {Function} setBillingData A function for setting billing data.
|
||||
* @property {Function} setShippingAddress A function for setting shipping address.
|
||||
* @property {boolean} shippingAsBilling A boolean which tracks if the customer is using the same billing and shipping address.
|
||||
* @property {Function} setShippingAsBilling A function for toggling shipping as billing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ShippingDataContext
|
||||
*
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
import classNames from 'classnames';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -13,10 +13,10 @@ export type CheckboxControlProps = {
|
|||
className?: string;
|
||||
label?: string;
|
||||
id?: string;
|
||||
instanceId: string;
|
||||
onChange: ( value: boolean ) => void;
|
||||
children: React.ReactChildren;
|
||||
hasError: boolean;
|
||||
children?: React.ReactChildren;
|
||||
hasError?: boolean;
|
||||
checked?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -26,12 +26,13 @@ export const CheckboxControl = ( {
|
|||
className,
|
||||
label,
|
||||
id,
|
||||
instanceId,
|
||||
onChange,
|
||||
children,
|
||||
hasError = false,
|
||||
checked = false,
|
||||
...rest
|
||||
}: CheckboxControlProps ): JSX.Element => {
|
||||
const instanceId = useInstanceId( CheckboxControl );
|
||||
const checkboxId = id || `checkbox-control-${ instanceId }`;
|
||||
|
||||
return (
|
||||
|
@ -51,6 +52,7 @@ export const CheckboxControl = ( {
|
|||
type="checkbox"
|
||||
onChange={ ( event ) => onChange( event.target.checked ) }
|
||||
aria-invalid={ hasError === true }
|
||||
checked={ checked }
|
||||
{ ...rest }
|
||||
/>
|
||||
<svg
|
||||
|
@ -72,4 +74,4 @@ export const CheckboxControl = ( {
|
|||
);
|
||||
};
|
||||
|
||||
export default withInstanceId( CheckboxControl );
|
||||
export default CheckboxControl;
|
||||
|
|
Loading…
Reference in New Issue