rebase on oct-10-2023

This commit is contained in:
Rachit Bhargava
2023-10-10 17:23:21 -04:00
parent d37566ffb6
commit d096058d7d
4789 changed files with 254611 additions and 307223 deletions

View File

@@ -11,11 +11,11 @@ import type {
CartShippingPackageShippingRate,
CartShippingRate,
} from '@woocommerce/types';
import { camelCase, mapKeys } from 'lodash';
import { BillingAddress, ShippingAddress } from '@woocommerce/settings';
import {
triggerAddedToCartEvent,
triggerAddingToCartEvent,
camelCaseKeys,
} from '@woocommerce/base-utils';
/**
@@ -70,9 +70,7 @@ export const setErrorData = (
export const receiveCartContents = (
response: CartResponse
): { type: string; response: Partial< Cart > } => {
const cart = mapKeys( response, ( _, key ) =>
camelCase( key )
) as unknown as Cart;
const cart = camelCaseKeys( response ) as unknown as Cart;
const { shippingAddress, billingAddress, ...cartWithoutAddress } = cart;
return {
type: types.SET_CART_DATA,
@@ -385,7 +383,7 @@ export const changeCartItemQuantity =
* @param {number | string} [packageId] The key of the packages that we will select within.
*/
export const selectShippingRate =
( rateId: string, packageId = 0 ) =>
( rateId: string, packageId: number | null = null ) =>
async ( {
dispatch,
select,
@@ -481,13 +479,6 @@ export const updateCustomerData =
}
};
export const setFullShippingAddressPushed = (
fullShippingAddressPushed: boolean
) => ( {
type: types.SET_FULL_SHIPPING_ADDRESS_PUSHED,
fullShippingAddressPushed,
} );
type Actions =
| typeof addItemToCart
| typeof applyCoupon
@@ -508,7 +499,6 @@ type Actions =
| typeof setShippingAddress
| typeof shippingRatesBeingSelected
| typeof updateCustomerData
| typeof setFullShippingAddressPushed
| typeof updatingCustomerData;
export type CartAction = ReturnOrGeneratorYieldUnion< Actions | Thunks >;

View File

@@ -100,7 +100,6 @@ export const defaultCartState: CartState = {
applyingCoupon: '',
removingCoupon: '',
isCartDataStale: false,
fullShippingAddressPushed: false,
},
errors: EMPTY_CART_ERRORS,
};

View File

@@ -32,6 +32,7 @@ const registeredStore = registerStore< State >( STORE_KEY, {
__experimentalUseThunks: true,
} );
// Pushes changes whenever the store is updated.
registeredStore.subscribe( pushChanges );
// This will skip the debounce and immediately push changes to the server when a field is blurred.

View File

@@ -25,7 +25,7 @@ export const notifyCartErrors = (
createNotice( 'error', decodeEntities( error.message ), {
id: error.code,
context: 'wc/cart',
isDismissible: true,
isDismissible: false,
} );
}
} );

View File

@@ -1,271 +1,181 @@
/**
* External dependencies
*/
import { debounce, pick } from 'lodash';
import { select, dispatch } from '@wordpress/data';
import { pluckEmail, removeAllNotices } from '@woocommerce/base-utils';
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';
import { VALIDATION_STORE_KEY } from '../validation';
import { processErrorResponse } from '../utils';
import { shippingAddressHasValidationErrors } from './utils';
import { getDirtyKeys, validateDirtyProps, BaseAddressKey } 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;
// 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[],
},
};
/**
* Trims and normalizes address data for comparison.
* Initializes the customer data cache on the first run.
*/
export const normalizeAddress = ( address: BillingOrShippingAddress ) => {
const trimmedAddress = Object.entries( address ).reduce(
( acc, [ key, value ] ) => {
if ( key === 'postcode' ) {
acc[ key as keyof BillingOrShippingAddress ] = value
.replace( ' ', '' )
.toUpperCase();
} else {
acc[ key as keyof BillingOrShippingAddress ] = value.trim();
}
return acc;
},
{} as BillingOrShippingAddress
);
return trimmedAddress;
const initialize = () => {
localState.customerData = select( STORE_KEY ).getCustomerData();
localState.customerDataIsInitialized = true;
};
/**
* Does a shallow compare of all address data to determine if the cart needs updating on the server.
* Checks customer data against new customer data to get a list of dirty props.
*/
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 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;
};
/**
* Function to dispatch an update to the server.
*/
const updateCustomerData = (): void => {
if ( localState.doingPush ) {
return;
}
const addressMatches = isShallowEqual(
normalizeAddress( previousAddress ),
normalizeAddress( address )
);
// Prevent multiple pushes from happening at the same time.
localState.doingPush = true;
return ! addressMatches;
};
// Get updated list of dirty props by comparing customer data.
updateDirtyProps();
type BaseAddressKey = keyof CartBillingAddress | keyof CartShippingAddress;
// Do we need to push anything?
const needsPush =
localState.dirtyProps.billingAddress.length > 0 ||
localState.dirtyProps.shippingAddress.length > 0;
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[];
if ( ! needsPush ) {
localState.doingPush = false;
return;
}
>{
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 ) {
// 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 ( dirtyProps.billingAddress.length ) {
if ( localState.dirtyProps.billingAddress.length ) {
customerDataToUpdate.billing_address = pick(
billingAddress,
dirtyProps.billingAddress
localState.customerData.billingAddress,
localState.dirtyProps.billingAddress
);
dirtyProps.billingAddress = [];
}
if ( dirtyProps.shippingAddress.length ) {
if ( localState.dirtyProps.shippingAddress.length ) {
customerDataToUpdate.shipping_address = pick(
shippingAddress,
dirtyProps.shippingAddress
localState.customerData.shippingAddress,
localState.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 ) => {
processErrorResponse( response );
dispatch( STORE_KEY )
.updateCustomerData( customerDataToUpdate )
.then( () => {
localState.dirtyProps.billingAddress = [];
localState.dirtyProps.shippingAddress = [];
localState.doingPush = false;
removeAllNotices();
} )
.catch( ( response ) => {
localState.doingPush = false;
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[] ),
];
}
} )
.finally( () => {
if ( ! shippingAddressHasValidationErrors() ) {
dispatch( STORE_KEY ).setFullShippingAddressPushed( true );
}
} );
/**
* Function to dispatch an update to the server. This is debounced.
*/
const debouncedUpdateCustomerData = debounce( () => {
if ( localState.doingPush ) {
debouncedUpdateCustomerData();
return;
}
}, 1000 );
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 = (): void => {
const store = select( STORE_KEY );
if ( ! store.hasFinishedResolution( 'getCartData' ) ) {
export const pushChanges = ( debounced = true ): void => {
if ( ! select( STORE_KEY ).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;
if ( ! localState.customerDataIsInitialized ) {
initialize();
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
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 => {
updateCustomerData.flush();
debouncedUpdateCustomerData.flush();
};

View File

@@ -48,15 +48,6 @@ const reducer: Reducer< CartState > = (
action: Partial< CartAction >
) => {
switch ( action.type ) {
case types.SET_FULL_SHIPPING_ADDRESS_PUSHED:
state = {
...state,
metaData: {
...state.metaData,
fullShippingAddressPushed: action.fullShippingAddressPushed,
},
};
break;
case types.SET_ERROR_DATA:
if ( action.error ) {
state = {

View File

@@ -9,7 +9,6 @@ import { CartResponse } from '@woocommerce/types';
*/
import { CART_API_ERROR } from './constants';
import type { CartDispatchFromMap, CartResolveSelectFromMap } from './index';
import { shippingAddressHasValidationErrors } from './utils';
/**
* Resolver for retrieving all cart data.
@@ -28,10 +27,6 @@ export const getCartData =
receiveError( CART_API_ERROR );
return;
}
if ( ! shippingAddressHasValidationErrors() ) {
dispatch.setFullShippingAddressPushed( true );
}
receiveCart( cartData );
};

View File

@@ -222,10 +222,3 @@ export const getItemsPendingQuantityUpdate = ( state: CartState ): string[] => {
export const getItemsPendingDelete = ( state: CartState ): string[] => {
return state.cartItemsPendingDelete;
};
/**
* Whether the address has changes that have not been synced with the server.
*/
export const getFullShippingAddressPushed = ( state: CartState ): boolean => {
return state.metaData.fullShippingAddressPushed;
};

View File

@@ -3,14 +3,17 @@
*/
import { dispatch, select } from '@wordpress/data';
import { previewCart } from '@woocommerce/resource-previews';
import { camelCase, cloneDeep, mapKeys } from 'lodash';
import { Cart, CartResponse } from '@woocommerce/types';
import { Cart } from '@woocommerce/types';
import { camelCaseKeys } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import { notifyQuantityChanges } from '../notify-quantity-changes';
// Deep clone an object to avoid mutating it later.
const cloneObject = ( obj ) => JSON.parse( JSON.stringify( obj ) );
jest.mock( '@wordpress/data' );
const mockedCreateInfoNotice = jest.fn();
@@ -34,14 +37,8 @@ select.mockImplementation( () => {
* Clones the preview cart and turns it into a `Cart`.
*/
const getFreshCarts = (): { oldCart: Cart; newCart: Cart } => {
const oldCart = mapKeys(
cloneDeep< CartResponse >( previewCart ),
( _, key ) => camelCase( key )
) as unknown as Cart;
const newCart = mapKeys(
cloneDeep< CartResponse >( previewCart ),
( _, key ) => camelCase( key )
) as unknown as Cart;
const oldCart = camelCaseKeys( cloneObject( previewCart ) ) as Cart;
const newCart = camelCaseKeys( cloneObject( previewCart ) ) as Cart;
return { oldCart, newCart };
};

View File

@@ -45,17 +45,10 @@ jest.mock( '@wordpress/data', () => ( {
dispatch: jest.fn(),
} ) );
// Mocking lodash here so we can just call the debounced function directly without waiting for debounce.
jest.mock( 'lodash', () => ( {
...jest.requireActual( 'lodash' ),
__esModule: true,
debounce: jest.fn( ( callback ) => callback ),
} ) );
// Mocking processErrorResponse because we don't actually care about processing the error response, we just don't want
// pushChanges to throw an error.
jest.mock( '../utils', () => ( {
...jest.requireActual( '../utils' ),
jest.mock( '../../utils', () => ( {
...jest.requireActual( '../../utils' ),
__esModule: true,
processErrorResponse: jest.fn(),
} ) );
@@ -84,7 +77,7 @@ describe( 'pushChanges', () => {
...jest
.requireActual( '@wordpress/data' )
.select( storeName ),
getValidationError: () => undefined,
getValidationError: jest.fn().mockReturnValue( undefined ),
};
}
return jest.requireActual( '@wordpress/data' ).select( storeName );
@@ -105,7 +98,7 @@ describe( 'pushChanges', () => {
} );
it( 'Keeps props dirty if data did not persist due to an error', async () => {
// Run this without changing anything because the first run does not push data (the first run is populating what was received on page load).
pushChanges();
pushChanges( false );
// Mock the returned value of `getCustomerData` to simulate a change in the shipping address.
getCustomerDataMock.mockReturnValue( {
@@ -135,7 +128,7 @@ describe( 'pushChanges', () => {
} );
// Push these changes to the server, the `updateCustomerData` mock is set to reject (in the original mock at the top of the file), to simulate a server error.
pushChanges();
pushChanges( false );
// Check that the mock was called with only the updated data.
await expect( updateCustomerDataMock ).toHaveBeenCalledWith( {
@@ -182,7 +175,7 @@ describe( 'pushChanges', () => {
// Although only one property was updated between calls, we should expect City, State, and Postcode to be pushed
// to the server because the previous push failed when they were originally changed.
pushChanges();
pushChanges( false );
await expect( updateCustomerDataMock ).toHaveBeenLastCalledWith( {
shipping_address: {
city: 'Houston',

View File

@@ -7,7 +7,7 @@ import {
ApiErrorResponse,
isApiErrorResponse,
} from '@woocommerce/types';
import { camelCase, mapKeys } from 'lodash';
import { camelCaseKeys } from '@woocommerce/base-utils';
/**
* Internal dependencies
@@ -31,9 +31,7 @@ export const receiveCart =
dispatch: CartDispatchFromMap;
select: CartSelectFromMap;
} ) => {
const newCart = mapKeys( response, ( _, key ) =>
camelCase( key )
) as unknown as Cart;
const newCart = camelCaseKeys( response ) as unknown as Cart;
const oldCart = select.getCartData();
notifyCartErrors( newCart.errors, oldCart.errors );
notifyQuantityChanges( {

View File

@@ -2,7 +2,7 @@
* External dependencies
*/
import { dispatch, select } from '@wordpress/data';
import { debounce } from 'lodash';
import { debounce } from '@woocommerce/base-utils';
/**
* Internal dependencies

View File

@@ -1,9 +1,15 @@
/**
* External dependencies
*/
import { camelCase, mapKeys } from 'lodash';
import { Cart, CartResponse } from '@woocommerce/types';
import { select } from '@wordpress/data';
import { camelCaseKeys } from '@woocommerce/base-utils';
import { isEmail } from '@wordpress/url';
import {
CartBillingAddress,
CartShippingAddress,
Cart,
CartResponse,
} from '@woocommerce/types';
/**
* Internal dependencies
@@ -11,9 +17,7 @@ import { select } from '@wordpress/data';
import { STORE_KEY as VALIDATION_STORE_KEY } from '../validation/constants';
export const mapCartResponseToCart = ( responseCart: CartResponse ): Cart => {
return mapKeys( responseCart, ( _, key ) =>
camelCase( key )
) as unknown as Cart;
return camelCaseKeys( responseCart ) as unknown as Cart;
};
export const shippingAddressHasValidationErrors = () => {
@@ -38,3 +42,77 @@ export const shippingAddressHasValidationErrors = () => {
postcodeValidationErrors,
].some( ( entry ) => typeof entry !== 'undefined' );
};
export type BaseAddressKey =
| keyof CartBillingAddress
| keyof CartShippingAddress;
/**
* Normalizes address values before push.
*/
export const normalizeAddressProp = (
key: BaseAddressKey,
value?: string | undefined
) => {
// Skip normalizing for any non string field
if ( typeof value !== 'string' ) {
return value;
}
if ( key === 'email' ) {
return isEmail( value ) ? value.trim() : '';
}
if ( key === 'postcode' ) {
return value.replace( ' ', '' ).toUpperCase();
}
return value.trim();
};
/**
* Compares two address objects and returns an array of keys that have changed.
*/
export const getDirtyKeys = <
T extends CartBillingAddress & CartShippingAddress
>(
// An object containing all previous address information
previousAddress: Partial< T >,
// An object containing all address information.
address: Partial< T >
): BaseAddressKey[] => {
const previousAddressKeys = Object.keys(
previousAddress
) as BaseAddressKey[];
return previousAddressKeys.filter( ( key: BaseAddressKey ) => {
return (
normalizeAddressProp( key, previousAddress[ key ] ) !==
normalizeAddressProp( key, address[ key ] )
);
} );
};
/**
* Validates dirty props before push.
*/
export const validateDirtyProps = ( dirtyProps: {
billingAddress: BaseAddressKey[];
shippingAddress: BaseAddressKey[];
} ): boolean => {
const validationStore = select( VALIDATION_STORE_KEY );
const invalidProps = [
...dirtyProps.billingAddress.filter( ( key ) => {
return (
validationStore.getValidationError( 'billing_' + key ) !==
undefined
);
} ),
...dirtyProps.shippingAddress.filter( ( key ) => {
return (
validationStore.getValidationError( 'shipping_' + key ) !==
undefined
);
} ),
].filter( Boolean );
return invalidProps.length === 0;
};

View File

@@ -2,6 +2,7 @@
* External dependencies
*/
import {
CanMakePaymentArgument,
ExpressPaymentMethodConfigInstance,
PaymentMethodConfigInstance,
} from '@woocommerce/types';
@@ -31,54 +32,12 @@ import {
} from '../../../data/constants';
import { defaultCartState } from '../../../data/cart/default-state';
const registrationErrorNotice = (
paymentMethod:
| ExpressPaymentMethodConfigInstance
| PaymentMethodConfigInstance,
errorMessage: string,
express = false
) => {
const { createErrorNotice } = dispatch( 'core/notices' );
const noticeContext = express
? noticeContexts.EXPRESS_PAYMENTS
: noticeContexts.PAYMENTS;
const errorText = sprintf(
/* translators: %s the id of the payment method being registered (bank transfer, cheque...) */
__(
`There was an error registering the payment method with id '%s': `,
'woo-gutenberg-products-block'
),
paymentMethod.paymentMethodId
);
createErrorNotice( `${ errorText } ${ errorMessage }`, {
context: noticeContext,
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
} );
};
export const checkPaymentMethodsCanPay = async ( express = false ) => {
/**
* Get the argument that will be passed to a payment method's `canMakePayment` method.
*/
export const getCanMakePaymentArg = (): CanMakePaymentArgument => {
const isEditor = !! select( 'core/editor' );
let availablePaymentMethods = {};
const paymentMethods = express
? getExpressPaymentMethods()
: getPaymentMethods();
const addAvailablePaymentMethod = (
paymentMethod:
| PaymentMethodConfigInstance
| ExpressPaymentMethodConfigInstance
) => {
const { name } = paymentMethod;
availablePaymentMethods = {
...availablePaymentMethods,
[ paymentMethod.name ]: { name },
};
};
let cartForCanPayArgument: Record< string, unknown > = {};
let canPayArgument: Record< string, unknown > = {};
let canPayArgument: CanMakePaymentArgument;
if ( ! isEditor ) {
const store = select( CART_STORE_KEY );
@@ -91,7 +50,7 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
cart.shippingRates
);
cartForCanPayArgument = {
const cartForCanPayArgument = {
cartCoupons: cart.coupons,
cartItems: cart.items,
crossSellsProducts: cart.crossSells,
@@ -126,7 +85,7 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
paymentRequirements: cart.paymentRequirements,
};
} else {
cartForCanPayArgument = {
const cartForCanPayArgument = {
cartCoupons: previewCart.coupons,
cartItems: previewCart.items,
crossSellsProducts: previewCart.cross_sells,
@@ -151,8 +110,8 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
};
canPayArgument = {
cart: cartForCanPayArgument,
cartTotals: cartForCanPayArgument.totals,
cartNeedsShipping: cartForCanPayArgument.needsShipping,
cartTotals: cartForCanPayArgument.cartTotals,
cartNeedsShipping: cartForCanPayArgument.cartNeedsShipping,
billingData: cartForCanPayArgument.billingAddress,
billingAddress: cartForCanPayArgument.billingAddress,
shippingAddress: cartForCanPayArgument.shippingAddress,
@@ -164,16 +123,65 @@ export const checkPaymentMethodsCanPay = async ( express = false ) => {
};
}
return canPayArgument;
};
const registrationErrorNotice = (
paymentMethod:
| ExpressPaymentMethodConfigInstance
| PaymentMethodConfigInstance,
errorMessage: string,
express = false
) => {
const { createErrorNotice } = dispatch( 'core/notices' );
const noticeContext = express
? noticeContexts.EXPRESS_PAYMENTS
: noticeContexts.PAYMENTS;
const errorText = sprintf(
/* translators: %s the id of the payment method being registered (bank transfer, cheque...) */
__(
`There was an error registering the payment method with id '%s': `,
'woo-gutenberg-products-block'
),
paymentMethod.paymentMethodId
);
createErrorNotice( `${ errorText } ${ errorMessage }`, {
context: noticeContext,
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
} );
};
export const checkPaymentMethodsCanPay = async ( express = false ) => {
let availablePaymentMethods = {};
const paymentMethods = express
? getExpressPaymentMethods()
: getPaymentMethods();
const addAvailablePaymentMethod = (
paymentMethod:
| PaymentMethodConfigInstance
| ExpressPaymentMethodConfigInstance
) => {
const { name } = paymentMethod;
availablePaymentMethods = {
...availablePaymentMethods,
[ paymentMethod.name ]: { name },
};
};
// Order payment methods.
const paymentMethodsOrder = express
? Object.keys( paymentMethods )
: Array.from(
new Set( [
...( getSetting( 'paymentGatewaySortOrder', [] ) as [] ),
...( getSetting( 'paymentMethodSortOrder', [] ) as [] ),
...Object.keys( paymentMethods ),
] )
);
const canPayArgument = getCanMakePaymentArg();
const cartPaymentMethods = canPayArgument.paymentMethods as string[];
const isEditor = !! select( 'core/editor' );
for ( let i = 0; i < paymentMethodsOrder.length; i++ ) {
const paymentMethodName = paymentMethodsOrder[ i ];

View File

@@ -1,16 +0,0 @@
/**
* External dependencies
*/
import { has } from 'lodash';
/**
* Utility for returning whether the given path exists in the state.
*
* @param {Object} state The state being checked
* @param {Array} path The path to check
*
* @return {boolean} True means this exists in the state.
*/
export default function hasInState( state, path ) {
return has( state, path );
}

View File

@@ -0,0 +1,27 @@
const has = ( obj: Record< string, unknown >, path: string[] ): boolean => {
return (
!! path &&
!! path.reduce< unknown >(
( prevObj, key ) =>
typeof prevObj === 'object' && prevObj !== null
? ( prevObj as Record< string, unknown > )[ key ]
: undefined,
obj
)
);
};
/**
* Utility for returning whether the given path exists in the state.
*
* @param {Object} state The state being checked
* @param {Array} path The path to check
*
* @return {boolean} True means this exists in the state.
*/
export default function hasInState(
state: Record< string, unknown >,
path: string[]
): boolean {
return has( state, path );
}

View File

@@ -1,17 +0,0 @@
/**
* External dependencies
*/
import { setWith, clone } from 'lodash';
/**
* Utility for updating state and only cloning objects in the path that changed.
*
* @param {Object} state The state being updated
* @param {Array} path The path being updated
* @param {*} value The value to update for the path
*
* @return {Object} The new state
*/
export default function updateState( state, path, value ) {
return setWith( clone( state ), path, value, clone );
}

View File

@@ -0,0 +1,37 @@
/**
* Utility for updating nested state in the path that changed.
*/
function updateNested< T >( // The state being updated
state: T,
// The path being updated
path: string[],
// The value to update for the path
value: unknown,
// The current index in the path
index = 0
): T {
const key = path[ index ] as keyof T;
if ( index === path.length - 1 ) {
return { ...state, [ key ]: value };
}
const nextState = state[ key ] || {};
return {
...state,
[ key ]: updateNested( nextState, path, value, index + 1 ),
} as T;
}
/**
* Utility for updating state and only cloning objects in the path that changed.
*/
export default function updateState< T >(
// The state being updated
state: T,
// The path being updated
path: string[],
// The value to update for the path
value: unknown
): T {
return updateNested( state, path, value );
}

View File

@@ -2,7 +2,6 @@
* External dependencies
*/
import type { Reducer } from 'redux';
import { pickBy } from 'lodash';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { isString, FieldValidationStatus } from '@woocommerce/types';
@@ -19,16 +18,24 @@ const reducer: Reducer< Record< string, FieldValidationStatus > > = (
const newState = { ...state };
switch ( action.type ) {
case types.SET_VALIDATION_ERRORS:
const newErrors = pickBy( action.errors, ( error, property ) => {
if ( typeof error?.message !== 'string' ) {
return false;
if ( ! action.errors ) {
return state;
}
const hasNewError = Object.entries( action.errors ).some(
( [ property, error ] ) => {
if ( typeof error?.message !== 'string' ) {
return false;
}
if (
state.hasOwnProperty( property ) &&
isShallowEqual( state[ property ], error )
) {
return false;
}
return true;
}
if ( state.hasOwnProperty( property ) ) {
return ! isShallowEqual( state[ property ], error );
}
return true;
} );
if ( Object.values( newErrors ).length === 0 ) {
);
if ( ! hasNewError ) {
return state;
}
return { ...state, ...action.errors };