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

279 lines
7.4 KiB
TypeScript
Raw Normal View History

/**
* External dependencies
*/
import { debounce, pick } from 'lodash';
import { select, dispatch } from '@wordpress/data';
import { pluckEmail, removeAllNotices } from '@woocommerce/base-utils';
import {
CartBillingAddress,
CartShippingAddress,
BillingAddressShippingAddress,
} from '@woocommerce/types';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
*/
import { STORE_KEY } from './constants';
import { VALIDATION_STORE_KEY } from '../validation';
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';
Remove WC Core shipping settings if Cart/Checkout blocks are in use (https://github.com/woocommerce/woocommerce-blocks/pull/8679) * Add CartCheckoutUtils class This class will store reusable methods relating to Cart/Checkout Blocks, i.e. whether they are used on the Cart/Checkout page. * Update ShippingController to use the new CartCheckoutUtils function This will reduce code duplication when checking if the Cart/Checkout blocks are in use on the Cart/Checkout page. * Add filter to remove shipping settings when Cart/Checkout are default * Ensure setting displays correctly if cart is default but not checkout * Add tests to ensure core shipping settings update correctly * Add setCartCheckoutPages function to update set the cart/checkout page * Force shipping to be enabled if the Checkout block is in use. * Add filter to override cost requires address option * Add shippingCostRequiresAddress option * Check if the address is required before showing rates * Show shipping rates in editor * Add shippingCostRequiresAddress attribute to shipping methods block * Update frontend type to show shippingCostRequiresAddress is a prop * Add control to toggle shippingCostRequiresAddress option * Show address notice in the correct scenario * Send shippingCostRequiresAddress to Block in front end context * Add e2e test for editor control * Add e2e tests for shipping options on the front end * Add updateAttributeInSiblingBlock function * Add shippingCostRequiresAddress to shipping method block * Ensure attribute is updated in both blocks when editing * In Shipping Methods Block, show correct component based on block setting * Show correct block in editor * Remove broken test from PR * Clean up updateAttributeInSiblingBlock * Add setCartCheckoutPages function to update set the cart/checkout page * Add tests to ensure core shipping settings update correctly * Add isAddressComplete function Borrowed from woocommerce/woocommerce-blocks#8141 * Check if the address is required before showing rates * Show shipping rates in editor * Show address notice in the correct scenario * Add e2e tests for shipping options on the front end * Ensure errorId is passed to StateInput * Add fullShippingAddressPushed action to wc/store/cart * Add fullShippingAddressPushed case to reducer * Ensure fullShippingAddressPushed is set when initialising cart store * Add fullShippingAddressPushed selector and default state entry * Add shippingAddressHasValidationErrors util function * Do not overwrite addresses when selecting a rate * Set whether full address has been pushed when saving address changes * In Shipping Methods Block, show correct component based on block setting * Don't show from price if rates should be hidden until address entered * Check city validation errors to assert if shipping address is valid * Rename merchant.js to merchant.ts * Move local pickup functions to common merchant util * Update local pickup tests to use common merchant utils * Add test to ensure setting toggles in both blocks * Add navigating to settings and saving in merchant util * Create addPickupLocation merchant util * Add test for local pickup and require full address * Make sure correct conditions are met to show shipping options * Ensure checkbox is checked during local pickup tests * Unset the checkbox when tests are finished running * Update checkout block fixture * Prevent error in unit tests * Import validation store key from constants Required because importing from the index causes the validation data store to register twice * Update checkout terms test to wait for button not to be disabled * Revert "Add isAddressComplete function" This reverts commit 9967dc0d4f10cf638859ae085e6f4cc2901dd299.
2023-03-13 11:49:28 +00:00
import { shippingAddressHasValidationErrors } from './utils';
type CustomerData = {
billingAddress: CartBillingAddress;
shippingAddress: CartShippingAddress;
};
type BillingOrShippingAddress = CartBillingAddress | CartShippingAddress;
/**
* Checks if a cart response contains an email property.
*/
const isBillingAddress = (
address: BillingOrShippingAddress
): address is CartBillingAddress => {
return 'email' in address;
};
/**
* Trims and normalizes address data for comparison.
*/
export const normalizeAddress = ( address: BillingOrShippingAddress ) => {
const trimmedAddress = Object.entries( address ).reduce(
( acc, [ key, value ] ) => {
//Skip normalizing for any non string field
if ( typeof value !== 'string' ) {
acc[ key as keyof BillingOrShippingAddress ] = value;
return acc;
}
if ( key === 'postcode' ) {
acc[ key as keyof BillingOrShippingAddress ] = value
.replace( ' ', '' )
.toUpperCase();
return acc;
}
acc[ key as keyof BillingOrShippingAddress ] = value.trim();
return acc;
},
{} as BillingOrShippingAddress
);
return trimmedAddress;
};
/**
* Does a shallow compare of all address data to determine if the cart needs updating on the server.
*/
const isAddressDirty = < T extends CartBillingAddress | CartShippingAddress >(
// An object containing all previous address information
previousAddress: T,
// An object containing all address information.
address: T
): boolean => {
if (
isBillingAddress( address ) &&
pluckEmail( address ) !==
pluckEmail( previousAddress as CartBillingAddress )
) {
return true;
}
const addressMatches = isShallowEqual(
normalizeAddress( previousAddress ),
normalizeAddress( address )
);
return ! addressMatches;
};
type BaseAddressKey = keyof CartBillingAddress | keyof CartShippingAddress;
const getDirtyKeys = < T extends CartBillingAddress | CartShippingAddress >(
// An object containing all previous address information
previousAddress: T,
// An object containing all address information.
address: T
): BaseAddressKey[] => {
const previousAddressKeys = Object.keys(
previousAddress
) as BaseAddressKey[];
return previousAddressKeys.filter( ( key ) => {
return previousAddress[ key ] !== address[ key ];
} );
};
/**
* Local cache of customerData used for comparisons.
*/
let customerData = <CustomerData>{
billingAddress: {},
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 = <
{
billingAddress: BaseAddressKey[];
shippingAddress: BaseAddressKey[];
}
>{
billingAddress: [],
shippingAddress: [],
};
/**
* Function to dispatch an update to the server. This is debounced.
*/
const updateCustomerData = debounce( (): void => {
const { billingAddress, shippingAddress } = customerData;
const validationStore = select( VALIDATION_STORE_KEY );
// Before we push anything, we need to ensure that the data we're pushing (dirty fields) are valid, otherwise we will
// abort and wait for the validation issues to be resolved.
const invalidProps = [
...dirtyProps.billingAddress.filter( ( key ) => {
return (
validationStore.getValidationError( 'billing_' + key ) !==
undefined
);
} ),
...dirtyProps.shippingAddress.filter( ( key ) => {
return (
validationStore.getValidationError( 'shipping_' + key ) !==
undefined
);
} ),
].filter( Boolean );
if ( invalidProps.length ) {
return;
}
// Find valid data from the list of dirtyProps and prepare to push to the server.
const customerDataToUpdate = {} as Partial< BillingAddressShippingAddress >;
if ( dirtyProps.billingAddress.length ) {
customerDataToUpdate.billing_address = pick(
billingAddress,
dirtyProps.billingAddress
);
dirtyProps.billingAddress = [];
}
if ( dirtyProps.shippingAddress.length ) {
customerDataToUpdate.shipping_address = pick(
shippingAddress,
dirtyProps.shippingAddress
);
dirtyProps.shippingAddress = [];
}
// If there is customer data to update, push it to the server.
if ( Object.keys( customerDataToUpdate ).length ) {
dispatch( STORE_KEY )
.updateCustomerData( customerDataToUpdate )
.then( removeAllNotices )
.catch( ( response ) => {
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
processErrorResponse( response );
// Data did not persist due to an error. Make the props dirty again so they get pushed to the server.
if ( customerDataToUpdate.billing_address ) {
dirtyProps.billingAddress = [
...dirtyProps.billingAddress,
...( Object.keys(
customerDataToUpdate.billing_address
) as BaseAddressKey[] ),
];
}
if ( customerDataToUpdate.shipping_address ) {
dirtyProps.shippingAddress = [
...dirtyProps.shippingAddress,
...( Object.keys(
customerDataToUpdate.shipping_address
) as BaseAddressKey[] ),
];
}
Remove WC Core shipping settings if Cart/Checkout blocks are in use (https://github.com/woocommerce/woocommerce-blocks/pull/8679) * Add CartCheckoutUtils class This class will store reusable methods relating to Cart/Checkout Blocks, i.e. whether they are used on the Cart/Checkout page. * Update ShippingController to use the new CartCheckoutUtils function This will reduce code duplication when checking if the Cart/Checkout blocks are in use on the Cart/Checkout page. * Add filter to remove shipping settings when Cart/Checkout are default * Ensure setting displays correctly if cart is default but not checkout * Add tests to ensure core shipping settings update correctly * Add setCartCheckoutPages function to update set the cart/checkout page * Force shipping to be enabled if the Checkout block is in use. * Add filter to override cost requires address option * Add shippingCostRequiresAddress option * Check if the address is required before showing rates * Show shipping rates in editor * Add shippingCostRequiresAddress attribute to shipping methods block * Update frontend type to show shippingCostRequiresAddress is a prop * Add control to toggle shippingCostRequiresAddress option * Show address notice in the correct scenario * Send shippingCostRequiresAddress to Block in front end context * Add e2e test for editor control * Add e2e tests for shipping options on the front end * Add updateAttributeInSiblingBlock function * Add shippingCostRequiresAddress to shipping method block * Ensure attribute is updated in both blocks when editing * In Shipping Methods Block, show correct component based on block setting * Show correct block in editor * Remove broken test from PR * Clean up updateAttributeInSiblingBlock * Add setCartCheckoutPages function to update set the cart/checkout page * Add tests to ensure core shipping settings update correctly * Add isAddressComplete function Borrowed from woocommerce/woocommerce-blocks#8141 * Check if the address is required before showing rates * Show shipping rates in editor * Show address notice in the correct scenario * Add e2e tests for shipping options on the front end * Ensure errorId is passed to StateInput * Add fullShippingAddressPushed action to wc/store/cart * Add fullShippingAddressPushed case to reducer * Ensure fullShippingAddressPushed is set when initialising cart store * Add fullShippingAddressPushed selector and default state entry * Add shippingAddressHasValidationErrors util function * Do not overwrite addresses when selecting a rate * Set whether full address has been pushed when saving address changes * In Shipping Methods Block, show correct component based on block setting * Don't show from price if rates should be hidden until address entered * Check city validation errors to assert if shipping address is valid * Rename merchant.js to merchant.ts * Move local pickup functions to common merchant util * Update local pickup tests to use common merchant utils * Add test to ensure setting toggles in both blocks * Add navigating to settings and saving in merchant util * Create addPickupLocation merchant util * Add test for local pickup and require full address * Make sure correct conditions are met to show shipping options * Ensure checkbox is checked during local pickup tests * Unset the checkbox when tests are finished running * Update checkout block fixture * Prevent error in unit tests * Import validation store key from constants Required because importing from the index causes the validation data store to register twice * Update checkout terms test to wait for button not to be disabled * Revert "Add isAddressComplete function" This reverts commit 9967dc0d4f10cf638859ae085e6f4cc2901dd299.
2023-03-13 11:49:28 +00:00
} )
.finally( () => {
if ( ! shippingAddressHasValidationErrors() ) {
dispatch( STORE_KEY ).setFullShippingAddressPushed( true );
}
} );
}
}, 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 );
if ( ! store.hasFinishedResolution( 'getCartData' ) ) {
return;
}
// Returns all current customer data from the store.
const newCustomerData = store.getCustomerData();
// On first run, this will populate the customerData cache with the current customer data in the store.
// This does not need to be pushed to the server because it's already there.
if ( ! customerDataIsInitialized ) {
customerData = newCustomerData;
customerDataIsInitialized = true;
return;
}
// Check if the billing and shipping addresses are "dirty"--as in, they've changed since the last push.
const billingIsDirty = isAddressDirty(
customerData.billingAddress,
newCustomerData.billingAddress
);
const shippingIsDirty = isAddressDirty(
customerData.shippingAddress,
newCustomerData.shippingAddress
);
// Update local cache of dirty prop keys.
if ( billingIsDirty ) {
dirtyProps.billingAddress = [
...dirtyProps.billingAddress,
...getDirtyKeys(
customerData.billingAddress,
newCustomerData.billingAddress
),
];
}
if ( shippingIsDirty ) {
dirtyProps.shippingAddress = [
...dirtyProps.shippingAddress,
...getDirtyKeys(
customerData.shippingAddress,
newCustomerData.shippingAddress
),
];
}
// Update local cache of customer data so the next time this runs, it can compare against the latest data.
customerData = newCustomerData;
// Trigger the update if we have any dirty props.
if (
dirtyProps.billingAddress.length ||
dirtyProps.shippingAddress.length
) {
updateCustomerData();
}
};
export const flushChanges = (): void => {
updateCustomerData.flush();
};