Update canMakePayment to receive cart as argument and make it react to changes in billingData. (https://github.com/woocommerce/woocommerce-blocks/pull/4776)

* Add 	cartCoupons to canMakePayment argument

* Add cart to the paymentMethodArgument and improve the dependencies for the effect calling refreshCanMakePayments

* Debounce refreshCanMakePayments

The initial approach was to debounce billingData and use this value as a dependency for the useEffect that runs refreshCanMakePayments.
But because the depencies array can always change we decided to debounce the callback instead, ensuring this way that callback is not called multiple times: for example when typing a field in the billing address. Debounced was chosen instead of throttle because we want to call refreshCanMakePayments once the change event has stopped, with the final value.

* Update types and docs related to canMakePaymentArgument

* Mock the /cart call when testing payment methods

* Remove unused cartCoupons key in canMakePaymentArguments' interface
This commit is contained in:
Raluca Stan 2021-09-23 16:27:02 +01:00 committed by GitHub
parent 63582e47b2
commit 27600b3309
7 changed files with 65 additions and 32 deletions

View File

@ -26,7 +26,7 @@ import type {
CartResponseBillingAddress,
CartResponseShippingAddress,
CartResponseCouponItem,
CartResponseCouponItemWithLabel,
CartResponseCoupons,
} from '@woocommerce/types';
import {
emptyHiddenAddressFields,
@ -129,6 +129,7 @@ export const defaultCartData: StoreCart = {
*
* @return {StoreCart} Object containing cart data.
*/
export const useStoreCart = (
options: { shouldSelect: boolean } = { shouldSelect: true }
): StoreCart => {
@ -136,7 +137,6 @@ export const useStoreCart = (
const previewCart = previewData?.previewCart;
const { shouldSelect } = options;
const currentResults = useRef();
const results: StoreCart = useSelect(
( select, { dispatch } ) => {
if ( ! shouldSelect ) {
@ -194,7 +194,7 @@ export const useStoreCart = (
// Add a text property to the coupon to allow extensions to modify
// the text used to display the coupon, without affecting the
// functionality when it comes to removing the coupon.
const cartCoupons: CartResponseCouponItemWithLabel[] =
const cartCoupons: CartResponseCoupons =
cartData.coupons.length > 0
? cartData.coupons.map(
( coupon: CartResponseCouponItem ) => ( {

View File

@ -15,6 +15,7 @@ import type {
PaymentMethodConfigInstance,
ExpressPaymentMethodConfigInstance,
} from '@woocommerce/type-defs/payments';
import { useDebouncedCallback } from 'use-debounce';
/**
* Internal dependencies
@ -51,12 +52,10 @@ const usePaymentMethodRegistration = (
const { billingData, shippingAddress } = useCustomerDataContext();
const selectedShippingMethods = useShallowEqual( selectedRates );
const paymentMethodsOrder = useShallowEqual( paymentMethodsSortOrder );
const {
cartTotals,
cartNeedsShipping,
paymentRequirements,
} = useStoreCart();
const cart = useStoreCart();
const { cartTotals, cartNeedsShipping, paymentRequirements } = cart;
const canPayArgument = useRef( {
cart,
cartTotals,
cartNeedsShipping,
billingData,
@ -68,6 +67,7 @@ const usePaymentMethodRegistration = (
useEffect( () => {
canPayArgument.current = {
cart,
cartTotals,
cartNeedsShipping,
billingData,
@ -76,6 +76,7 @@ const usePaymentMethodRegistration = (
paymentRequirements,
};
}, [
cart,
cartTotals,
cartNeedsShipping,
billingData,
@ -160,16 +161,21 @@ const usePaymentMethodRegistration = (
registeredPaymentMethods,
] );
const [ debouncedRefreshCanMakePayments ] = useDebouncedCallback(
refreshCanMakePayments,
500
);
// Determine which payment methods are available initially and whenever
// shipping methods or cart totals change.
// shipping methods, cart or the billing data change.
// Some payment methods (e.g. COD) can be disabled for specific shipping methods.
useEffect( () => {
refreshCanMakePayments();
debouncedRefreshCanMakePayments();
}, [
refreshCanMakePayments,
cartTotals,
debouncedRefreshCanMakePayments,
cart,
selectedShippingMethods,
paymentRequirements,
billingData,
] );
return isInitialized;

View File

@ -2,6 +2,9 @@
* External dependencies
*/
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { previewCart } from '@woocommerce/resource-previews';
import { dispatch } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import {
registerPaymentMethod,
__experimentalDeRegisterPaymentMethod,
@ -15,6 +18,7 @@ import {
* Internal dependencies
*/
import PaymentMethods from '../payment-methods';
import { defaultCartState } from '../../../../data/default-states';
jest.mock( '../saved-payment-method-options', () => ( { onChange } ) => {
return (
@ -63,6 +67,22 @@ const resetMockPaymentMethods = () => {
};
describe( 'PaymentMethods', () => {
beforeEach( async () => {
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/cart/ ) ) {
return Promise.resolve( JSON.stringify( previewCart ) );
}
return Promise.resolve( '' );
} );
// need to clear the store resolution state between tests.
await dispatch( storeKey ).invalidateResolutionForStore();
await dispatch( storeKey ).receiveCart( defaultCartState.cartData );
} );
afterEach( () => {
fetchMock.resetMocks();
} );
test( 'should show no payment methods component when there are no payment methods', async () => {
render(
<PaymentMethodDataProvider>
@ -78,6 +98,8 @@ describe( 'PaymentMethods', () => {
// creates an extra `div` with the notice contents used for a11y.
expect( noPaymentMethods.length ).toBeGreaterThanOrEqual( 1 );
} );
// ["`select` control in `@wordpress/data-controls` is deprecated. Please use built-in `resolveSelect` control in `@wordpress/data` instead."]
expect( console ).toHaveWarned();
} );
test( 'selecting new payment method', async () => {

View File

@ -26,6 +26,8 @@ export interface CartResponseCouponItemWithLabel
label: string;
}
export type CartResponseCoupons = CartResponseCouponItemWithLabel[];
export interface ResponseFirstNameLastName {
first_name: string;
last_name: string;

View File

@ -11,6 +11,7 @@ import type {
CartResponseBillingAddress,
CartResponseShippingRate,
CartResponse,
CartResponseCoupons,
} from './cart-response';
import type { ResponseError } from '../../data/types';
export interface StoreCartItemQuantity {
@ -31,7 +32,7 @@ export interface StoreCartCoupon {
}
export interface StoreCart {
cartCoupons: Array< CartResponseCouponItem >;
cartCoupons: CartResponseCoupons;
cartItems: Array< CartResponseItem >;
cartFees: Array< CartResponseFeeItem >;
cartItemsCount: number;

View File

@ -6,7 +6,7 @@ import type { ReactNode } from 'react';
/**
* Internal dependencies
*/
import type { CartTotals } from './cart';
import type { CartTotals, Cart } from './cart';
import {
CartResponseBillingAddress,
CartResponseShippingAddress,
@ -27,6 +27,7 @@ export interface Supports extends SupportsConfiguration {
}
export interface CanMakePaymentArgument {
cart: Cart;
cartTotals: CartTotals;
cartNeedsShipping: boolean;
billingData: CartResponseBillingAddress;

View File

@ -4,21 +4,21 @@ The checkout block has an API interface for payment methods to integrate that co
## Table of Contents <!-- omit in toc -->
- [Client Side integration](#client-side-integration)
- [Express payment methods - `registerExpressPaymentMethod( options )`](#express-payment-methods---registerexpresspaymentmethod-options-)
- [`name` (required)](#name-required)
- [`content` (required)](#content-required)
- [`edit` (required)](#edit-required)
- [`canMakePayment` (required):](#canmakepayment-required)
- [`paymentMethodId`](#paymentmethodid)
- [`supports:features`](#supportsfeatures)
- [Payment Methods - `registerPaymentMethod( options )`](#payment-methods---registerpaymentmethod-options-)
- [Props Fed to Payment Method Nodes](#props-fed-to-payment-method-nodes)
- [Server Side Integration](#server-side-integration)
- [Processing Payment](#processing-payment)
- [Registering Assets](#registering-assets)
- [Hooking into the Checkout processing by the Store API.](#hooking-into-the-checkout-processing-by-the-store-api)
- [Putting it all together](#putting-it-all-together)
- [Client Side integration](#client-side-integration)
- [Express payment methods - `registerExpressPaymentMethod( options )`](#express-payment-methods---registerexpresspaymentmethod-options-)
- [`name` (required)](#name-required)
- [`content` (required)](#content-required)
- [`edit` (required)](#edit-required)
- [`canMakePayment` (required):](#canmakepayment-required)
- [`paymentMethodId`](#paymentmethodid)
- [`supports:features`](#supportsfeatures)
- [Payment Methods - `registerPaymentMethod( options )`](#payment-methods---registerpaymentmethod-options-)
- [Props Fed to Payment Method Nodes](#props-fed-to-payment-method-nodes)
- [Server Side Integration](#server-side-integration)
- [Processing Payment](#processing-payment)
- [Registering Assets](#registering-assets)
- [Hooking into the Checkout processing by the Store API.](#hooking-into-the-checkout-processing-by-the-store-api)
- [Putting it all together](#putting-it-all-together)
## Client Side integration
@ -52,7 +52,7 @@ The registry function expects a JavaScript object with options specific to the p
registerExpressPaymentMethod( options );
```
The options you feed the configuration instance should be an object in this shape (see `ExpressPaymentMethodRegistrationOptions` typedef):
The options you feed the configuration instance should be an object in this shape (see `ExpressPaymentMethodConfiguration` typedef):
```js
const options = {
@ -87,11 +87,12 @@ A callback to determine whether the payment method should be available as an opt
```
canMakePayment( {
cart: Cart,
cartTotals: CartTotals,
cartNeedsShipping: boolean,
shippingAddress: CartShippingAddress,
billingData: BillingData,
selectedShippingMethods: string[],
selectedShippingMethods: Record<string,unknown>,
paymentRequirements: string[],
} )
```