woocommerce/plugins/woocommerce-blocks/assets/js/data/cart/push-changes.ts

216 lines
6.1 KiB
TypeScript
Raw Normal View History

/**
* External dependencies
*/
import { removeAllNotices, debounce, pick } from '@woocommerce/base-utils';
import {
CartBillingAddress,
CartShippingAddress,
BillingAddressShippingAddress,
} from '@woocommerce/types';
import { select, dispatch } from '@wordpress/data';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
*/
import { STORE_KEY } from './constants';
New contexts for `StoreNoticesContainer` and notice grouping (https://github.com/woocommerce/woocommerce-blocks/pull/7711) * Refactor Store Notices Move snackbar hiding filter before notice creation Implements showApplyCouponNotice Refactor context providers Use STORE_NOTICE_CONTEXTS use refs to track notice containers Refactor ref usage Use existing noticeContexts * Move new notice code to checkout package * Combine store and snackbars * Update noticeContexts imports * Remove context provider * Update data store * Fix 502 * Add new error contexts * Force types * Unnecessary reorder of imports * Fix global handling * Document forceType * Optional props are undefined * Remove function name * Missing condition * Remove context prop * Define ACTION_TYPES * Remove controls * Update assets/js/base/context/event-emit/utils.ts Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * CONTACT_INFORMATION * Remove ref from registerContainer * Abstract container locating methods * pass context correctly when displaying notices * Remove debugging buttons * Update filter usage - remove useMemo so filter can work inline * Refactor existing error notices from the API (https://github.com/woocommerce/woocommerce-blocks/pull/7728) * Update API type defs * Move create notice utils * Replace useCheckoutNotices with new contexts * processCheckoutResponseHeaders should check headers are defined * Scroll to error notices only if we're not editing a field * Error handling utils * processErrorResponse when pushing changes * processErrorResponse when processing checkout * remove formatStoreApiErrorMessage * Add todo for cart errors * Remove unused deps * unused imports * Fix linting warnings * Unused dep * Update assets/js/types/type-defs/api-response.ts Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Add todo * Use generic * remove const * Update array types * Phone should be in address blocks Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Update store name to wc/store/store-notices * Fix assertResponseIsValid * Funnel woocommerce_rest_invalid_email_address to the correct place * woocommerce_rest_missing_email_address * Move comments around * Move data back into const * Spacing * Remove spacing * Remove forced snack bar and styling * Move notices within wrapper * Remove type * hasStoreNoticesContainer rename * Group by status/context * Remove global context * Remove white space * remove changes to simplify diff * white space * Move comment to typescript * List style * showApplyCouponNotice docs * See if scrollIntoView exists * fix notice tests Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
2022-12-19 15:30:13 +00:00
import { processErrorResponse } from '../utils';
import { getDirtyKeys, validateDirtyProps, BaseAddressKey } from './utils';
// This is used to track and cache the local state of push changes.
const localState = {
// True when the customer data has been initialized.
customerDataIsInitialized: false,
// True when a push is currently happening to avoid simultaneous pushes.
doingPush: false,
// Local cache of the last pushed customerData used for comparisons.
customerData: {
billingAddress: {} as CartBillingAddress,
shippingAddress: {} as CartShippingAddress,
},
// Tracks which props have changed so the correct data gets pushed to the server.
dirtyProps: {
billingAddress: [] as BaseAddressKey[],
shippingAddress: [] as BaseAddressKey[],
},
2023-04-28 10:29:45 +00:00
};
/**
* Initializes the customer data cache on the first run.
*/
const initialize = () => {
localState.customerData = select( STORE_KEY ).getCustomerData();
localState.customerDataIsInitialized = true;
};
/**
* Checks customer data against new customer data to get a list of dirty props.
*/
const updateDirtyProps = () => {
// Returns all current customer data from the store.
const newCustomerData = select( STORE_KEY ).getCustomerData();
localState.dirtyProps.billingAddress = [
...localState.dirtyProps.billingAddress,
...getDirtyKeys(
localState.customerData.billingAddress,
newCustomerData.billingAddress
),
];
localState.dirtyProps.shippingAddress = [
...localState.dirtyProps.shippingAddress,
...getDirtyKeys(
localState.customerData.shippingAddress,
newCustomerData.shippingAddress
),
];
// Update local cache of customer data so the next time this runs, it can compare against the latest data.
localState.customerData = newCustomerData;
const dirtyShippingAddress = localState.dirtyProps.shippingAddress;
const dirtyBillingAddress = localState.dirtyProps.billingAddress;
const customerShippingAddress = localState.customerData.shippingAddress;
const customerBillingAddress = localState.customerData.billingAddress;
// Check if country is changing without state
const shippingCountryChanged = dirtyShippingAddress.includes( 'country' );
const billingCountryChanged = dirtyBillingAddress.includes( 'country' );
const shippingStateChanged = dirtyShippingAddress.includes( 'state' );
const billingStateChanged = dirtyBillingAddress.includes( 'state' );
const shippingPostcodeChanged = dirtyShippingAddress.includes( 'postcode' );
const billingPostcodeChanged = dirtyBillingAddress.includes( 'postcode' );
if ( shippingCountryChanged && ! shippingPostcodeChanged ) {
dirtyShippingAddress.push( 'postcode' );
customerShippingAddress.postcode = '';
}
if ( billingCountryChanged && ! billingPostcodeChanged ) {
dirtyBillingAddress.push( 'postcode' );
customerBillingAddress.postcode = '';
}
if ( shippingCountryChanged && ! shippingStateChanged ) {
dirtyShippingAddress.push( 'state' );
customerShippingAddress.state = '';
}
if ( billingCountryChanged && ! billingStateChanged ) {
dirtyBillingAddress.push( 'state' );
customerBillingAddress.state = '';
}
};
/**
* Function to dispatch an update to the server.
*/
const updateCustomerData = (): void => {
if ( localState.doingPush ) {
return;
}
// Prevent multiple pushes from happening at the same time.
localState.doingPush = true;
// Get updated list of dirty props by comparing customer data.
updateDirtyProps();
// Do we need to push anything?
const needsPush =
localState.dirtyProps.billingAddress.length > 0 ||
localState.dirtyProps.shippingAddress.length > 0;
if ( ! needsPush ) {
localState.doingPush = false;
return;
}
// Check props are valid, or abort.
if ( ! validateDirtyProps( localState.dirtyProps ) ) {
localState.doingPush = false;
return;
}
// Find valid data from the list of dirtyProps and prepare to push to the server.
const customerDataToUpdate = {} as Partial< BillingAddressShippingAddress >;
if ( localState.dirtyProps.billingAddress.length ) {
customerDataToUpdate.billing_address = pick(
localState.customerData.billingAddress,
localState.dirtyProps.billingAddress
);
}
if ( localState.dirtyProps.shippingAddress.length ) {
customerDataToUpdate.shipping_address = pick(
localState.customerData.shippingAddress,
localState.dirtyProps.shippingAddress
);
}
dispatch( STORE_KEY )
.updateCustomerData( customerDataToUpdate )
.then( () => {
localState.dirtyProps.billingAddress = [];
localState.dirtyProps.shippingAddress = [];
localState.doingPush = false;
removeAllNotices();
} )
.catch( ( response ) => {
localState.doingPush = false;
processErrorResponse( response );
} );
};
/**
* Function to dispatch an update to the server. This is debounced.
*/
const debouncedUpdateCustomerData = debounce( () => {
if ( localState.doingPush ) {
debouncedUpdateCustomerData();
return;
}
updateCustomerData();
}, 1500 );
/**
* 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.
*
* Any update to the store triggers this, so we do a shallow compare on the important data to know if we really need to
* schedule a push.
*/
export const pushChanges = ( debounced = true ): void => {
if ( ! select( STORE_KEY ).hasFinishedResolution( 'getCartData' ) ) {
return;
}
if ( ! localState.customerDataIsInitialized ) {
initialize();
return;
}
if (
isShallowEqual(
localState.customerData,
select( STORE_KEY ).getCustomerData()
)
) {
return;
}
if ( debounced ) {
debouncedUpdateCustomerData();
} else {
updateCustomerData();
}
};
// Cancel the debounced updateCustomerData function and trigger it immediately.
export const flushChanges = (): void => {
debouncedUpdateCustomerData.flush();
};