rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -0,0 +1,18 @@
export enum ACTION_TYPES {
SET_PAYMENT_IDLE = 'SET_PAYMENT_IDLE',
SET_EXPRESS_PAYMENT_STARTED = 'SET_EXPRESS_PAYMENT_STARTED',
SET_PAYMENT_READY = 'SET_PAYMENT_READY',
SET_PAYMENT_PROCESSING = 'SET_PAYMENT_PROCESSING',
SET_PAYMENT_ERROR = 'SET_PAYMENT_ERROR',
SET_PAYMENT_METHODS_INITIALIZED = 'SET_PAYMENT_METHODS_INITIALIZED',
SET_EXPRESS_PAYMENT_METHODS_INITIALIZED = 'SET_EXPRESS_PAYMENT_METHODS_INITIALIZED',
SET_ACTIVE_PAYMENT_METHOD = 'SET_ACTIVE_PAYMENT_METHOD',
SET_SHOULD_SAVE_PAYMENT_METHOD = 'SET_SHOULD_SAVE_PAYMENT_METHOD',
SET_AVAILABLE_PAYMENT_METHODS = 'SET_AVAILABLE_PAYMENT_METHODS',
SET_AVAILABLE_EXPRESS_PAYMENT_METHODS = 'SET_AVAILABLE_EXPRESS_PAYMENT_METHODS',
REMOVE_AVAILABLE_PAYMENT_METHOD = 'REMOVE_AVAILABLE_PAYMENT_METHOD',
REMOVE_AVAILABLE_EXPRESS_PAYMENT_METHOD = 'REMOVE_AVAILABLE_EXPRESS_PAYMENT_METHOD',
INITIALIZE_PAYMENT_METHODS = 'INITIALIZE_PAYMENT_METHODS',
SET_PAYMENT_METHOD_DATA = 'SET_PAYMENT_METHOD_DATA',
SET_PAYMENT_RESULT = 'SET_PAYMENT_RESULT',
}

View File

@@ -0,0 +1,189 @@
/**
* External dependencies
*/
import {
PlainPaymentMethods,
PlainExpressPaymentMethods,
} from '@woocommerce/types';
import type { PaymentResult } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { ACTION_TYPES } from './action-types';
import { checkPaymentMethodsCanPay } from './utils/check-payment-methods';
import { setDefaultPaymentMethod } from './utils/set-default-payment-method';
// `Thunks are functions that can be dispatched, similar to actions creators
export * from './thunks';
export const __internalSetPaymentIdle = () => ( {
type: ACTION_TYPES.SET_PAYMENT_IDLE,
} );
export const __internalSetExpressPaymentStarted = () => ( {
type: ACTION_TYPES.SET_EXPRESS_PAYMENT_STARTED,
} );
export const __internalSetPaymentProcessing = () => ( {
type: ACTION_TYPES.SET_PAYMENT_PROCESSING,
} );
export const __internalSetPaymentError = () => ( {
type: ACTION_TYPES.SET_PAYMENT_ERROR,
} );
export const __internalSetPaymentReady = () => ( {
type: ACTION_TYPES.SET_PAYMENT_READY,
} );
/**
* Set whether the payment methods have been initialised or not
*
* @param initialized True if the `checkCanPay` methods have been run on all available payment methods
*/
export const __internalSetPaymentMethodsInitialized = (
initialized: boolean
) => {
return async ( { select, dispatch } ) => {
// If the currently selected method is not in this new list, then we need to select a new one, or select a default.
const methods = select.getAvailablePaymentMethods();
if ( initialized ) {
await setDefaultPaymentMethod( methods );
}
dispatch( {
type: ACTION_TYPES.SET_PAYMENT_METHODS_INITIALIZED,
initialized,
} );
};
};
/**
* Set whether the express payment methods have been initialised or not
*
* @param initialized True if the `checkCanPay` methods have been run on all express available payment methods
*/
export const __internalSetExpressPaymentMethodsInitialized = (
initialized: boolean
) => ( {
type: ACTION_TYPES.SET_EXPRESS_PAYMENT_METHODS_INITIALIZED,
initialized,
} );
/**
* Set a flag for whether to save the current payment method for next time
*
* @param shouldSavePaymentMethod Whether to save the current payment method for next time
*/
export const __internalSetShouldSavePaymentMethod = (
shouldSavePaymentMethod: boolean
) => ( {
type: ACTION_TYPES.SET_SHOULD_SAVE_PAYMENT_METHOD,
shouldSavePaymentMethod,
} );
/**
* Set the payment method the user has chosen. This should change every time the user selects a new payment method
*
* @param activePaymentMethod The name of the payment method selected by the user
* @param paymentMethodData The extra data associated with a payment
*/
export const __internalSetActivePaymentMethod = (
activePaymentMethod: string,
paymentMethodData: Record< string, unknown > = {}
) => ( {
type: ACTION_TYPES.SET_ACTIVE_PAYMENT_METHOD,
activePaymentMethod,
paymentMethodData,
} );
/**
* Set the extra data for the chosen payment method
*
* @param paymentMethodData The extra data associated with a payment
*/
export const __internalSetPaymentMethodData = (
paymentMethodData: Record< string, unknown > = {}
) => ( {
type: ACTION_TYPES.SET_PAYMENT_METHOD_DATA,
paymentMethodData,
} );
/**
* Store the result of the payment attempt from the /checkout StoreApi call
*
* @param data The result of the payment attempt through the StoreApi /checkout endpoints
*/
export const __internalSetPaymentResult = ( data: PaymentResult ) => ( {
type: ACTION_TYPES.SET_PAYMENT_RESULT,
data,
} );
/**
* Set the available payment methods.
* An available payment method is one that has been validated and can make a payment.
*/
export const __internalSetAvailablePaymentMethods = (
paymentMethods: PlainPaymentMethods
) => {
return async ( { dispatch, select } ) => {
// If the currently selected method is not in this new list, then we need to select a new one, or select a default.
const activePaymentMethod = select.getActivePaymentMethod();
if ( ! ( activePaymentMethod in paymentMethods ) ) {
await setDefaultPaymentMethod( paymentMethods );
}
dispatch( {
type: ACTION_TYPES.SET_AVAILABLE_PAYMENT_METHODS,
paymentMethods,
} );
};
};
/**
* Set the available express payment methods.
* An available payment method is one that has been validated and can make a payment.
*/
export const __internalSetAvailableExpressPaymentMethods = (
paymentMethods: PlainExpressPaymentMethods
) => ( {
type: ACTION_TYPES.SET_AVAILABLE_EXPRESS_PAYMENT_METHODS,
paymentMethods,
} );
/**
* Remove a payment method name from the available payment methods.
* This is called when a payment method is removed from the registry.
*/
export const __internalRemoveAvailablePaymentMethod = ( name: string ) => ( {
type: ACTION_TYPES.REMOVE_AVAILABLE_PAYMENT_METHOD,
name,
} );
/**
* Remove an express payment method name from the available payment methods.
* This is called when an express payment method is removed from the registry.
*/
export const __internalRemoveAvailableExpressPaymentMethod = (
name: string
) => ( {
type: ACTION_TYPES.REMOVE_AVAILABLE_EXPRESS_PAYMENT_METHOD,
name,
} );
/**
* The store is initialised once we have checked whether the payment methods registered can pay or not
*/
export function __internalUpdateAvailablePaymentMethods() {
return async ( { select, dispatch } ) => {
const expressRegistered = await checkPaymentMethodsCanPay( true );
const registered = await checkPaymentMethodsCanPay( false );
const { paymentMethodsInitialized, expressPaymentMethodsInitialized } =
select;
if ( registered && ! paymentMethodsInitialized() ) {
dispatch( __internalSetPaymentMethodsInitialized( true ) );
}
if ( expressRegistered && ! expressPaymentMethodsInitialized() ) {
dispatch( __internalSetExpressPaymentMethodsInitialized( true ) );
}
};
}

View File

@@ -0,0 +1,9 @@
export const STORE_KEY = 'wc/store/payment';
export enum STATUS {
IDLE = 'idle',
EXPRESS_STARTED = 'express_started',
PROCESSING = 'processing',
READY = 'ready',
ERROR = 'has_error',
}

View File

@@ -0,0 +1,48 @@
/**
* External dependencies
*/
import type { EmptyObjectType, PaymentResult } from '@woocommerce/types';
import { getSetting } from '@woocommerce/settings';
import {
PlainPaymentMethods,
PlainExpressPaymentMethods,
} from '@woocommerce/types';
/**
* Internal dependencies
*/
import { SavedPaymentMethod } from './types';
import { STATUS as PAYMENT_STATUS } from './constants';
export interface PaymentState {
status: string;
activePaymentMethod: string;
activeSavedToken: string;
// Available payment methods are payment methods which have been validated and can make payment.
availablePaymentMethods: PlainPaymentMethods;
availableExpressPaymentMethods: PlainExpressPaymentMethods;
savedPaymentMethods:
| Record< string, SavedPaymentMethod[] >
| EmptyObjectType;
paymentMethodData: Record< string, unknown >;
paymentResult: PaymentResult | null;
paymentMethodsInitialized: boolean;
expressPaymentMethodsInitialized: boolean;
shouldSavePaymentMethod: boolean;
}
export const defaultPaymentState: PaymentState = {
status: PAYMENT_STATUS.IDLE,
activePaymentMethod: '',
activeSavedToken: '',
availablePaymentMethods: {},
availableExpressPaymentMethods: {},
savedPaymentMethods: getSetting<
Record< string, SavedPaymentMethod[] > | EmptyObjectType
>( 'customerPaymentMethods', {} ),
paymentMethodData: {},
paymentResult: null,
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
};

View File

@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import { createReduxStore, register } from '@wordpress/data';
import { controls as dataControls } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import reducer from './reducers';
import { STORE_KEY } from './constants';
import * as actions from './actions';
import { controls as sharedControls } from '../shared-controls';
import * as selectors from './selectors';
import { DispatchFromMap, SelectFromMap } from '../mapped-types';
export const config = {
reducer,
selectors,
actions,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
controls: { ...dataControls, ...sharedControls } as any,
__experimentalUseThunks: true,
};
const store = createReduxStore( STORE_KEY, config );
register( store );
declare module '@wordpress/data' {
function dispatch(
key: typeof STORE_KEY
): DispatchFromMap< typeof actions >;
function select( key: typeof STORE_KEY ): SelectFromMap<
typeof selectors
> & {
hasFinishedResolution: ( selector: string ) => boolean;
};
}
export const PAYMENT_STORE_KEY = STORE_KEY;

View File

@@ -0,0 +1,152 @@
/**
* External dependencies
*/
import type { Reducer } from 'redux';
import { objectHasProp, PaymentResult } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { defaultPaymentState, PaymentState } from './default-state';
import { ACTION_TYPES } from './action-types';
import { STATUS } from './constants';
const reducer: Reducer< PaymentState > = (
state = defaultPaymentState,
action
) => {
let newState = state;
switch ( action.type ) {
case ACTION_TYPES.SET_PAYMENT_IDLE:
newState = {
...state,
status: STATUS.IDLE,
};
break;
case ACTION_TYPES.SET_EXPRESS_PAYMENT_STARTED:
newState = {
...state,
status: STATUS.EXPRESS_STARTED,
};
break;
case ACTION_TYPES.SET_PAYMENT_PROCESSING:
newState = {
...state,
status: STATUS.PROCESSING,
};
break;
case ACTION_TYPES.SET_PAYMENT_READY:
newState = {
...state,
status: STATUS.READY,
};
break;
case ACTION_TYPES.SET_PAYMENT_ERROR:
newState = {
...state,
status: STATUS.ERROR,
};
break;
case ACTION_TYPES.SET_SHOULD_SAVE_PAYMENT_METHOD:
newState = {
...state,
shouldSavePaymentMethod: action.shouldSavePaymentMethod,
};
break;
case ACTION_TYPES.SET_PAYMENT_METHOD_DATA:
newState = {
...state,
paymentMethodData: action.paymentMethodData,
};
break;
case ACTION_TYPES.SET_PAYMENT_RESULT:
newState = {
...state,
paymentResult: action.data as PaymentResult,
};
break;
case ACTION_TYPES.REMOVE_AVAILABLE_PAYMENT_METHOD:
const previousAvailablePaymentMethods = {
...state.availablePaymentMethods,
};
delete previousAvailablePaymentMethods[ action.name ];
newState = {
...state,
availablePaymentMethods: {
...previousAvailablePaymentMethods,
},
};
break;
case ACTION_TYPES.REMOVE_AVAILABLE_EXPRESS_PAYMENT_METHOD:
const previousAvailableExpressPaymentMethods = {
...state.availablePaymentMethods,
};
delete previousAvailableExpressPaymentMethods[ action.name ];
newState = {
...state,
availableExpressPaymentMethods: {
...previousAvailableExpressPaymentMethods,
},
};
break;
case ACTION_TYPES.SET_PAYMENT_METHODS_INITIALIZED:
newState = {
...state,
paymentMethodsInitialized: action.initialized,
};
break;
case ACTION_TYPES.SET_EXPRESS_PAYMENT_METHODS_INITIALIZED:
newState = {
...state,
expressPaymentMethodsInitialized: action.initialized,
};
break;
case ACTION_TYPES.SET_AVAILABLE_PAYMENT_METHODS:
newState = {
...state,
availablePaymentMethods: action.paymentMethods,
};
break;
case ACTION_TYPES.SET_AVAILABLE_EXPRESS_PAYMENT_METHODS:
newState = {
...state,
availableExpressPaymentMethods: action.paymentMethods,
};
break;
case ACTION_TYPES.SET_ACTIVE_PAYMENT_METHOD:
const activeSavedToken =
typeof state.paymentMethodData === 'object' &&
objectHasProp( action.paymentMethodData, 'token' )
? action.paymentMethodData.token + ''
: '';
newState = {
...state,
activeSavedToken,
activePaymentMethod: action.activePaymentMethod,
paymentMethodData:
action.paymentMethodData || state.paymentMethodData,
};
break;
default:
return newState;
}
return newState;
};
export type State = ReturnType< typeof reducer >;
export default reducer;

View File

@@ -0,0 +1,231 @@
/**
* External dependencies
*/
import { objectHasProp } from '@woocommerce/types';
import deprecated from '@wordpress/deprecated';
import { getSetting } from '@woocommerce/settings';
import type { GlobalPaymentMethod } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { PaymentState } from './default-state';
import { filterActiveSavedPaymentMethods } from './utils/filter-active-saved-payment-methods';
import { STATUS as PAYMENT_STATUS } from './constants';
const globalPaymentMethods: Record< string, string > = {};
if ( getSetting( 'globalPaymentMethods' ) ) {
getSetting< GlobalPaymentMethod[] >( 'globalPaymentMethods' ).forEach(
( method ) => {
globalPaymentMethods[ method.id ] = method.title;
}
);
}
export const isPaymentPristine = ( state: PaymentState ) => {
deprecated( 'isPaymentPristine', {
since: '9.6.0',
alternative: 'isPaymentIdle',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return state.status === PAYMENT_STATUS.IDLE;
};
export const isPaymentIdle = ( state: PaymentState ) =>
state.status === PAYMENT_STATUS.IDLE;
export const isPaymentStarted = ( state: PaymentState ) => {
deprecated( 'isPaymentStarted', {
since: '9.6.0',
alternative: 'isExpressPaymentStarted',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return state.status === PAYMENT_STATUS.EXPRESS_STARTED;
};
export const isExpressPaymentStarted = ( state: PaymentState ) => {
return state.status === PAYMENT_STATUS.EXPRESS_STARTED;
};
export const isPaymentProcessing = ( state: PaymentState ) =>
state.status === PAYMENT_STATUS.PROCESSING;
export const isPaymentReady = ( state: PaymentState ) =>
state.status === PAYMENT_STATUS.READY;
export const isPaymentSuccess = ( state: PaymentState ) => {
deprecated( 'isPaymentSuccess', {
since: '9.6.0',
alternative: 'isPaymentReady',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return state.status === PAYMENT_STATUS.READY;
};
export const hasPaymentError = ( state: PaymentState ) =>
state.status === PAYMENT_STATUS.ERROR;
export const isPaymentFailed = ( state: PaymentState ) => {
deprecated( 'isPaymentFailed', {
since: '9.6.0',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return state.status === PAYMENT_STATUS.ERROR;
};
export const isExpressPaymentMethodActive = ( state: PaymentState ) => {
return Object.keys( state.availableExpressPaymentMethods ).includes(
state.activePaymentMethod
);
};
export const getActiveSavedToken = ( state: PaymentState ) => {
return typeof state.paymentMethodData === 'object' &&
objectHasProp( state.paymentMethodData, 'token' )
? state.paymentMethodData.token + ''
: '';
};
export const getActivePaymentMethod = ( state: PaymentState ) => {
return state.activePaymentMethod;
};
export const getAvailablePaymentMethods = ( state: PaymentState ) => {
return state.availablePaymentMethods;
};
export const getAvailableExpressPaymentMethods = ( state: PaymentState ) => {
return state.availableExpressPaymentMethods;
};
export const getPaymentMethodData = ( state: PaymentState ) => {
return state.paymentMethodData;
};
export const getIncompatiblePaymentMethods = ( state: PaymentState ) => {
const {
availablePaymentMethods,
availableExpressPaymentMethods,
paymentMethodsInitialized,
expressPaymentMethodsInitialized,
} = state;
if ( ! paymentMethodsInitialized || ! expressPaymentMethodsInitialized ) {
return {};
}
return Object.fromEntries(
Object.entries( globalPaymentMethods ).filter( ( [ k ] ) => {
return ! (
k in
{
...availablePaymentMethods,
...availableExpressPaymentMethods,
}
);
} )
);
};
export const getSavedPaymentMethods = ( state: PaymentState ) => {
return state.savedPaymentMethods;
};
/**
* Filters the list of saved payment methods and returns only the ones which
* are active and supported by the payment gateway
*/
export const getActiveSavedPaymentMethods = ( state: PaymentState ) => {
const availablePaymentMethodKeys = Object.keys(
state.availablePaymentMethods
);
return filterActiveSavedPaymentMethods(
availablePaymentMethodKeys,
state.savedPaymentMethods
);
};
export const paymentMethodsInitialized = ( state: PaymentState ) => {
return state.paymentMethodsInitialized;
};
export const expressPaymentMethodsInitialized = ( state: PaymentState ) => {
return state.expressPaymentMethodsInitialized;
};
/**
* @deprecated - Use these selectors instead: isPaymentIdle, isPaymentProcessing,
* hasPaymentError
*/
export const getCurrentStatus = ( state: PaymentState ) => {
deprecated( 'getCurrentStatus', {
since: '8.9.0',
alternative: 'isPaymentIdle, isPaymentProcessing, hasPaymentError',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/7666',
} );
return {
get isPristine() {
deprecated( 'isPristine', {
since: '9.6.0',
alternative: 'isIdle',
plugin: 'WooCommerce Blocks',
} );
return isPaymentIdle( state );
}, // isPristine is the same as isIdle.
isIdle: isPaymentIdle( state ),
isStarted: isExpressPaymentStarted( state ),
isProcessing: isPaymentProcessing( state ),
get isFinished() {
deprecated( 'isFinished', {
since: '9.6.0',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return hasPaymentError( state ) || isPaymentReady( state );
},
hasError: hasPaymentError( state ),
get hasFailed() {
deprecated( 'hasFailed', {
since: '9.6.0',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return hasPaymentError( state );
},
get isSuccessful() {
deprecated( 'isSuccessful', {
since: '9.6.0',
plugin: 'WooCommerce Blocks',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8110',
} );
return isPaymentReady( state );
},
isDoingExpressPayment: isExpressPaymentMethodActive( state ),
};
};
export const getShouldSavePaymentMethod = ( state: PaymentState ) => {
return state.shouldSavePaymentMethod;
};
export const getPaymentResult = ( state: PaymentState ) => {
return state.paymentResult;
};
// We should avoid using this selector and instead use the focused selectors
// We're keeping it because it's used in our unit test: assets/js/blocks/cart-checkout-shared/payment-methods/test/payment-methods.js
// to mock the selectors.
export const getState = ( state: PaymentState ) => {
return state;
};

View File

@@ -0,0 +1,45 @@
/**
* Internal dependencies
*/
import { setDefaultPaymentMethod as setDefaultPaymentMethodOriginal } from '../utils/set-default-payment-method';
import { PAYMENT_STORE_KEY } from '..';
import { PlainPaymentMethods } from '../../../types';
const originalDispatch = jest.requireActual( '@wordpress/data' ).dispatch;
jest.mock( '../utils/set-default-payment-method', () => ( {
setDefaultPaymentMethod: jest.fn(),
} ) );
describe( 'payment data store actions', () => {
const paymentMethods: PlainPaymentMethods = {
'wc-payment-gateway-1': {
name: 'wc-payment-gateway-1',
},
'wc-payment-gateway-2': {
name: 'wc-payment-gateway-2',
},
};
describe( 'setAvailablePaymentMethods', () => {
it( 'Does not call setDefaultPaymentGateway if the current method is still available', () => {
const actions = originalDispatch( PAYMENT_STORE_KEY );
actions.__internalSetActivePaymentMethod(
Object.keys( paymentMethods )[ 0 ]
);
actions.__internalSetAvailablePaymentMethods( paymentMethods );
expect( setDefaultPaymentMethodOriginal ).not.toBeCalled();
} );
it( 'Resets the default gateway if the current method is no longer available', () => {
const actions = originalDispatch( PAYMENT_STORE_KEY );
actions.__internalSetActivePaymentMethod(
Object.keys( paymentMethods )[ 0 ]
);
actions.__internalSetAvailablePaymentMethods( [
paymentMethods[ Object.keys( paymentMethods )[ 0 ] ],
] );
expect( setDefaultPaymentMethodOriginal ).toBeCalled();
} );
} );
} );

View File

@@ -0,0 +1,166 @@
/**
* External dependencies
*/
import * as wpDataFunctions from '@wordpress/data';
import { previewCart } from '@woocommerce/resource-previews';
import { PAYMENT_STORE_KEY, CART_STORE_KEY } from '@woocommerce/block-data';
import {
registerPaymentMethod,
registerExpressPaymentMethod,
__experimentalDeRegisterPaymentMethod,
__experimentalDeRegisterExpressPaymentMethod,
} from '@woocommerce/blocks-registry';
import { CanMakePaymentArgument } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { checkPaymentMethodsCanPay } from '../utils/check-payment-methods';
const requiredKeyCheck = ( args: CanMakePaymentArgument ) => {
const requiredKeys = [
'billingData',
'billingAddress',
'cart',
'cartNeedsShipping',
'cartTotals',
'paymentMethods',
'paymentRequirements',
'selectedShippingMethods',
'shippingAddress',
];
const argKeys = Object.keys( args );
const requiredCartKeys = [
'cartCoupons',
'cartItems',
'crossSellsProducts',
'cartFees',
'cartItemsCount',
'cartItemsWeight',
'cartNeedsPayment',
'cartNeedsShipping',
'cartItemErrors',
'cartTotals',
'cartIsLoading',
'cartErrors',
'billingData',
'billingAddress',
'shippingAddress',
'extensions',
'shippingRates',
'isLoadingRates',
'cartHasCalculatedShipping',
'paymentRequirements',
'receiveCart',
];
const cartKeys = Object.keys( args.cart );
const requiredTotalsKeys = [
'total_items',
'total_items_tax',
'total_fees',
'total_fees_tax',
'total_discount',
'total_discount_tax',
'total_shipping',
'total_shipping_tax',
'total_price',
'total_tax',
'tax_lines',
'currency_code',
'currency_symbol',
'currency_minor_unit',
'currency_decimal_separator',
'currency_thousand_separator',
'currency_prefix',
'currency_suffix',
];
const totalsKeys = Object.keys( args.cartTotals );
return (
requiredKeys.every( ( key ) => argKeys.includes( key ) ) &&
requiredTotalsKeys.every( ( key ) => totalsKeys.includes( key ) ) &&
requiredCartKeys.every( ( key ) => cartKeys.includes( key ) )
);
};
const mockedCanMakePayment = jest.fn().mockImplementation( requiredKeyCheck );
const mockedExpressCanMakePayment = jest
.fn()
.mockImplementation( requiredKeyCheck );
const registerMockPaymentMethods = ( savedCards = true ) => {
[ 'credit-card' ].forEach( ( name ) => {
registerPaymentMethod( {
name,
label: name,
content: <div>A payment method</div>,
edit: <div>A payment method</div>,
icons: null,
canMakePayment: mockedCanMakePayment,
supports: {
showSavedCards: savedCards,
showSaveOption: true,
features: [ 'products' ],
},
ariaLabel: name,
} );
} );
[ 'express-payment' ].forEach( ( name ) => {
const Content = ( {
onClose = () => void null,
onClick = () => void null,
} ) => {
return (
<>
<button onClick={ onClick }>
{ name + ' express payment method' }
</button>
<button onClick={ onClose }>
{ name + ' express payment method close' }
</button>
</>
);
};
registerExpressPaymentMethod( {
name,
content: <Content />,
edit: <div>An express payment method</div>,
canMakePayment: mockedExpressCanMakePayment,
paymentMethodId: name,
supports: {
features: [ 'products' ],
},
} );
} );
wpDataFunctions
.dispatch( PAYMENT_STORE_KEY )
.__internalUpdateAvailablePaymentMethods();
wpDataFunctions.dispatch( CART_STORE_KEY ).receiveCart( {
...previewCart,
payment_methods: [ 'cheque', 'bacs', 'credit-card' ],
} );
};
const resetMockPaymentMethods = () => {
[ 'cheque', 'bacs', 'credit-card' ].forEach( ( name ) => {
__experimentalDeRegisterPaymentMethod( name );
} );
[ 'express-payment' ].forEach( ( name ) => {
__experimentalDeRegisterExpressPaymentMethod( name );
} );
};
describe( 'checkPaymentMethods', () => {
beforeEach( registerMockPaymentMethods );
afterEach( resetMockPaymentMethods );
it( `Sends correct arguments to regular payment methods' canMakePayment functions`, async () => {
await checkPaymentMethodsCanPay();
expect( mockedCanMakePayment ).toHaveReturnedWith( true );
} );
it( `Sends correct arguments to express payment methods' canMakePayment functions`, async () => {
await checkPaymentMethodsCanPay( true );
expect( mockedExpressCanMakePayment ).toHaveReturnedWith( true );
} );
} );

View File

@@ -0,0 +1,211 @@
/**
* External dependencies
*/
import deepFreeze from 'deep-freeze';
/**
* Internal dependencies
*/
import reducer from '../reducers';
import { ACTION_TYPES } from '../action-types';
describe( 'paymentMethodDataReducer', () => {
const originalState = deepFreeze( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: {},
availableExpressPaymentMethods: {},
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
it( 'sets state as expected when adding a payment method', () => {
const nextState = reducer( originalState, {
type: ACTION_TYPES.SET_AVAILABLE_PAYMENT_METHODS,
paymentMethods: { 'my-new-method': { express: false } },
} );
expect( nextState ).toEqual( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: { 'my-new-method': { express: false } },
availableExpressPaymentMethods: {},
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
} );
it( 'sets state as expected when removing a payment method', () => {
const stateWithRegisteredMethod = deepFreeze( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: { 'my-new-method': { express: false } },
availableExpressPaymentMethods: {},
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
const nextState = reducer( stateWithRegisteredMethod, {
type: ACTION_TYPES.REMOVE_AVAILABLE_PAYMENT_METHOD,
name: 'my-new-method',
} );
expect( nextState ).toEqual( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: {},
availableExpressPaymentMethods: {},
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
} );
it( 'sets state as expected when adding an express payment method', () => {
const nextState = reducer( originalState, {
type: ACTION_TYPES.SET_AVAILABLE_EXPRESS_PAYMENT_METHODS,
paymentMethods: { 'my-new-method': { express: true } },
} );
expect( nextState ).toEqual( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: {},
availableExpressPaymentMethods: {
'my-new-method': { express: true },
},
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
} );
it( 'sets state as expected when removing an express payment method', () => {
const stateWithRegisteredMethod = deepFreeze( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: {},
availableExpressPaymentMethods: [ 'my-new-method' ],
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
const nextState = reducer( stateWithRegisteredMethod, {
type: ACTION_TYPES.REMOVE_AVAILABLE_EXPRESS_PAYMENT_METHOD,
name: 'my-new-method',
} );
expect( nextState ).toEqual( {
currentStatus: {
isPristine: true,
isStarted: false,
isProcessing: false,
isFinished: false,
hasError: false,
hasFailed: false,
isSuccessful: false,
},
availablePaymentMethods: {},
availableExpressPaymentMethods: {},
paymentMethodData: {},
paymentMethodsInitialized: false,
expressPaymentMethodsInitialized: false,
shouldSavePaymentMethod: false,
errorMessage: '',
activePaymentMethod: '',
activeSavedToken: '',
incompatiblePaymentMethods: {},
} );
} );
it( 'should handle SET_PAYMENT_RESULT', () => {
const mockResponse = {
message: 'success',
redirectUrl: 'https://example.com',
paymentStatus: 'not set',
paymentDetails: {},
};
const expectedState = {
...originalState,
paymentResult: mockResponse,
};
expect(
reducer( originalState, {
type: ACTION_TYPES.SET_PAYMENT_RESULT,
data: mockResponse,
} )
).toEqual( expectedState );
} );
} );

View File

@@ -0,0 +1,331 @@
/**
* External dependencies
*/
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { previewCart } from '@woocommerce/resource-previews';
import * as wpDataFunctions from '@wordpress/data';
import {
CART_STORE_KEY as storeKey,
PAYMENT_STORE_KEY,
} from '@woocommerce/block-data';
import {
registerPaymentMethod,
registerExpressPaymentMethod,
__experimentalDeRegisterPaymentMethod,
__experimentalDeRegisterExpressPaymentMethod,
} from '@woocommerce/blocks-registry';
import { default as fetchMock } from 'jest-fetch-mock';
/**
* Internal dependencies
*/
import {
CheckoutExpressPayment,
SavedPaymentMethodOptions,
} from '../../../blocks/cart-checkout-shared/payment-methods';
import { defaultCartState } from '../../cart/default-state';
const originalSelect = jest.requireActual( '@wordpress/data' ).select;
jest.spyOn( wpDataFunctions, 'select' ).mockImplementation( ( storeName ) => {
const originalStore = originalSelect( storeName );
if ( storeName === storeKey ) {
return {
...originalStore,
hasFinishedResolution: jest
.fn()
.mockImplementation( ( selectorName ) => {
if ( selectorName === 'getCartTotals' ) {
return true;
}
return originalStore.hasFinishedResolution( selectorName );
} ),
};
}
return originalStore;
} );
jest.mock( '@woocommerce/settings', () => {
const originalModule = jest.requireActual( '@woocommerce/settings' );
return {
// @ts-ignore We know @woocommerce/settings is an object.
...originalModule,
getSetting: ( setting, ...rest ) => {
if ( setting === 'customerPaymentMethods' ) {
return {
cc: [
{
method: {
gateway: 'credit-card',
last4: '4242',
brand: 'Visa',
},
expires: '12/22',
is_default: true,
tokenId: 1,
},
],
};
}
return originalModule.getSetting( setting, ...rest );
},
};
} );
const registerMockPaymentMethods = ( savedCards = true ) => {
[ 'cheque', 'bacs' ].forEach( ( name ) => {
registerPaymentMethod( {
name,
label: name,
content: <div>A payment method</div>,
edit: <div>A payment method</div>,
icons: null,
canMakePayment: () => true,
supports: {
features: [ 'products' ],
},
ariaLabel: name,
} );
} );
[ 'credit-card' ].forEach( ( name ) => {
registerPaymentMethod( {
name,
label: name,
content: <div>A payment method</div>,
edit: <div>A payment method</div>,
icons: null,
canMakePayment: () => true,
supports: {
showSavedCards: savedCards,
showSaveOption: true,
features: [ 'products' ],
},
ariaLabel: name,
} );
} );
[ 'express-payment' ].forEach( ( name ) => {
const Content = ( {
onClose = () => void null,
onClick = () => void null,
} ) => {
return (
<>
<button onClick={ onClick }>
{ name + ' express payment method' }
</button>
<button onClick={ onClose }>
{ name + ' express payment method close' }
</button>
</>
);
};
registerExpressPaymentMethod( {
name,
content: <Content />,
edit: <div>An express payment method</div>,
canMakePayment: () => true,
paymentMethodId: name,
supports: {
features: [ 'products' ],
},
} );
} );
wpDataFunctions
.dispatch( PAYMENT_STORE_KEY )
.__internalUpdateAvailablePaymentMethods();
};
const resetMockPaymentMethods = () => {
[ 'cheque', 'bacs', 'credit-card' ].forEach( ( name ) => {
__experimentalDeRegisterPaymentMethod( name );
} );
[ 'express-payment' ].forEach( ( name ) => {
__experimentalDeRegisterExpressPaymentMethod( name );
} );
};
describe( 'Payment method data store selectors/thunks', () => {
beforeEach( () => {
act( () => {
registerMockPaymentMethods( false );
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/v1\/cart/ ) ) {
return Promise.resolve( JSON.stringify( previewCart ) );
}
return Promise.resolve( '' );
} );
// need to clear the store resolution state between tests.
wpDataFunctions.dispatch( storeKey ).invalidateResolutionForStore();
wpDataFunctions
.dispatch( storeKey )
.receiveCart( defaultCartState.cartData );
} );
} );
afterEach( async () => {
act( () => {
resetMockPaymentMethods();
fetchMock.resetMocks();
} );
} );
it( 'toggles active payment method correctly for express payment activation and close', async () => {
const TriggerActiveExpressPaymentMethod = () => {
const activePaymentMethod = wpDataFunctions.useSelect(
( select ) => {
return select( PAYMENT_STORE_KEY ).getActivePaymentMethod();
}
);
return (
<>
<CheckoutExpressPayment />
{ 'Active Payment Method: ' + activePaymentMethod }
</>
);
};
const TestComponent = () => {
return <TriggerActiveExpressPaymentMethod />;
};
render( <TestComponent /> );
// should initialize by default the first payment method.
await waitFor( () => {
const activePaymentMethod = screen.queryByText(
/Active Payment Method: credit-card/
);
expect( activePaymentMethod ).not.toBeNull();
} );
// Express payment method clicked.
userEvent.click(
screen.getByText( 'express-payment express payment method' )
);
await waitFor( () => {
const activePaymentMethod = screen.queryByText(
/Active Payment Method: express-payment/
);
expect( activePaymentMethod ).not.toBeNull();
} );
// Express payment method closed.
userEvent.click(
screen.getByText( 'express-payment express payment method close' )
);
await waitFor( () => {
const activePaymentMethod = screen.queryByText(
/Active Payment Method: credit-card/
);
expect( activePaymentMethod ).not.toBeNull();
} );
} );
} );
describe( 'Testing Payment Methods work correctly with saved cards turned on', () => {
beforeEach( () => {
act( () => {
registerMockPaymentMethods( true );
fetchMock.mockResponse( ( req ) => {
if ( req.url.match( /wc\/store\/v1\/cart/ ) ) {
return Promise.resolve( JSON.stringify( previewCart ) );
}
return Promise.resolve( '' );
} );
// need to clear the store resolution state between tests.
wpDataFunctions.dispatch( storeKey ).invalidateResolutionForStore();
wpDataFunctions
.dispatch( storeKey )
.receiveCart( defaultCartState.cartData );
} );
} );
afterEach( async () => {
act( () => {
resetMockPaymentMethods();
fetchMock.resetMocks();
} );
} );
it( 'resets saved payment method data after starting and closing an express payment method', async () => {
const TriggerActiveExpressPaymentMethod = () => {
const { activePaymentMethod, paymentMethodData } =
wpDataFunctions.useSelect( ( select ) => {
const store = select( PAYMENT_STORE_KEY );
return {
activePaymentMethod: store.getActivePaymentMethod(),
paymentMethodData: store.getPaymentMethodData(),
};
} );
return (
<>
<CheckoutExpressPayment />
<SavedPaymentMethodOptions onChange={ () => void null } />
{ 'Active Payment Method: ' + activePaymentMethod }
{ paymentMethodData[ 'wc-credit-card-payment-token' ] && (
<span>credit-card token</span>
) }
</>
);
};
const TestComponent = () => {
return <TriggerActiveExpressPaymentMethod />;
};
render( <TestComponent /> );
// Should initialize by default the default saved payment method.
await waitFor( () => {
const activePaymentMethod = screen.queryByText(
/Active Payment Method: credit-card/
);
expect( activePaymentMethod ).not.toBeNull();
} );
await waitFor( () => {
const creditCardToken = screen.queryByText( /credit-card token/ );
expect( creditCardToken ).not.toBeNull();
} );
// Express payment method clicked.
userEvent.click(
screen.getByText( 'express-payment express payment method' )
);
await waitFor( () => {
const activePaymentMethod = screen.queryByText(
/Active Payment Method: express-payment/
);
expect( activePaymentMethod ).not.toBeNull();
} );
await waitFor( () => {
const creditCardToken = screen.queryByText( /credit-card token/ );
expect( creditCardToken ).toBeNull();
} );
// Express payment method closed.
userEvent.click(
screen.getByText( 'express-payment express payment method close' )
);
await waitFor( () => {
const activePaymentMethod = screen.queryByText(
/Active Payment Method: credit-card/
);
expect( activePaymentMethod ).not.toBeNull();
} );
await waitFor( () => {
const creditCardToken = screen.queryByText( /credit-card token/ );
expect( creditCardToken ).not.toBeNull();
} );
} );
} );

View File

@@ -0,0 +1,149 @@
/* eslint-disable no-unused-expressions */
/**
* External dependencies
*/
import * as wpDataFunctions from '@wordpress/data';
/**
* Internal dependencies
*/
import { setDefaultPaymentMethod } from '../utils/set-default-payment-method';
import { PlainPaymentMethods } from '../../../types';
import { PAYMENT_STORE_KEY } from '..';
const originalSelect = jest.requireActual( '@wordpress/data' ).select;
describe( 'setDefaultPaymentMethod', () => {
afterEach( () => {
jest.resetAllMocks();
jest.resetModules();
} );
const paymentMethods: PlainPaymentMethods = {
'wc-payment-gateway-1': {
name: 'wc-payment-gateway-1',
},
'wc-payment-gateway-2': {
name: 'wc-payment-gateway-2',
},
};
it( 'correctly sets the first payment method in the list of available payment methods', async () => {
jest.spyOn( wpDataFunctions, 'select' ).mockImplementation(
( storeName ) => {
const originalStore = originalSelect( storeName );
if ( storeName === PAYMENT_STORE_KEY ) {
return {
...originalStore,
getAvailableExpressPaymentMethods: () => {
return {
express_payment_1: {
name: 'express_payment_1',
},
};
},
getSavedPaymentMethods: () => {
return {};
},
};
}
return originalStore;
}
);
const originalDispatch =
jest.requireActual( '@wordpress/data' ).dispatch;
const setActivePaymentMethodMock = jest.fn();
jest.spyOn( wpDataFunctions, 'dispatch' ).mockImplementation(
( storeName ) => {
const originalStore = originalDispatch( storeName );
if ( storeName === PAYMENT_STORE_KEY ) {
return {
...originalStore,
__internalSetActivePaymentMethod:
setActivePaymentMethodMock,
};
}
return originalStore;
}
);
await setDefaultPaymentMethod( paymentMethods );
expect( setActivePaymentMethodMock ).toHaveBeenCalledWith(
'wc-payment-gateway-1'
);
} );
it( 'correctly sets the saved payment method if one is available', async () => {
jest.spyOn( wpDataFunctions, 'select' ).mockImplementation(
( storeName ) => {
const originalStore = originalSelect( storeName );
if ( storeName === PAYMENT_STORE_KEY ) {
return {
...originalStore,
getAvailableExpressPaymentMethods: () => {
return {
express_payment_1: {
name: 'express_payment_1',
},
};
},
getSavedPaymentMethods: () => {
return {
cc: [
{
method: {
gateway: 'saved-method',
last4: '4242',
brand: 'Visa',
},
expires: '04/44',
is_default: true,
actions: {
delete: {
url: 'https://example.com/delete',
name: 'Delete',
},
},
tokenId: 2,
},
],
};
},
};
}
return originalStore;
}
);
const originalDispatch =
jest.requireActual( '@wordpress/data' ).dispatch;
const setActivePaymentMethodMock = jest.fn();
jest.spyOn( wpDataFunctions, 'dispatch' ).mockImplementation(
( storeName ) => {
const originalStore = originalDispatch( storeName );
if ( storeName === PAYMENT_STORE_KEY ) {
return {
...originalStore,
__internalSetActivePaymentMethod:
setActivePaymentMethodMock,
__internalSetPaymentError: () => void 0,
__internalSetPaymentIdle: () => void 0,
__internalSetExpressPaymentStarted: () => void 0,
__internalSetPaymentProcessing: () => void 0,
__internalSetPaymentReady: () => void 0,
};
}
return originalStore;
}
);
await setDefaultPaymentMethod( paymentMethods );
expect( setActivePaymentMethodMock ).toHaveBeenCalledWith(
'saved-method',
{
isSavedToken: true,
payment_method: 'saved-method',
token: '2',
'wc-saved-method-payment-token': '2',
}
);
} );
} );

View File

@@ -0,0 +1,224 @@
/**
* External dependencies
*/
import * as wpDataFunctions from '@wordpress/data';
import { EventObserversType } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import { PAYMENT_STORE_KEY } from '../index';
import { __internalEmitPaymentProcessingEvent } from '../thunks';
/**
* If an observer returns billingAddress, shippingAddress, or paymentData, then the values of these
* should be updated in the data stores.
*/
const testShippingAddress = {
first_name: 'test',
last_name: 'test',
company: 'test',
address_1: 'test',
address_2: 'test',
city: 'test',
state: 'test',
postcode: 'test',
country: 'test',
phone: 'test',
};
const testBillingAddress = {
...testShippingAddress,
email: 'test@test.com',
};
const testPaymentMethodData = {
payment_method: 'test',
};
describe( 'wc/store/payment thunks', () => {
const testPaymentProcessingCallback = jest.fn();
const testPaymentProcessingCallback2 = jest.fn();
const currentObservers: EventObserversType = {
payment_setup: new Map(),
};
currentObservers.payment_setup.set( 'test', {
callback: testPaymentProcessingCallback,
priority: 10,
} );
currentObservers.payment_setup.set( 'test2', {
callback: testPaymentProcessingCallback2,
priority: 10,
} );
describe( '__internalEmitPaymentProcessingEvent', () => {
beforeEach( () => {
jest.resetAllMocks();
} );
it( 'calls all registered observers', async () => {
const {
__internalEmitPaymentProcessingEvent:
__internalEmitPaymentProcessingEventFromStore,
} = wpDataFunctions.dispatch( PAYMENT_STORE_KEY );
await __internalEmitPaymentProcessingEventFromStore(
currentObservers,
jest.fn()
);
expect( testPaymentProcessingCallback ).toHaveBeenCalled();
expect( testPaymentProcessingCallback2 ).toHaveBeenCalled();
} );
it( 'sets metadata if successful observers return it', async () => {
const testSuccessCallbackWithMetadata = jest.fn().mockReturnValue( {
type: 'success',
meta: {
billingAddress: testBillingAddress,
shippingAddress: testShippingAddress,
paymentMethodData: testPaymentMethodData,
},
} );
currentObservers.payment_setup.set( 'test3', {
callback: testSuccessCallbackWithMetadata,
priority: 10,
} );
const setBillingAddressMock = jest.fn();
const setShippingAddressMock = jest.fn();
const setPaymentMethodDataMock = jest.fn();
const registryMock = {
dispatch: jest.fn().mockImplementation( ( store: string ) => {
return {
...wpDataFunctions.dispatch( store ),
setBillingAddress: setBillingAddressMock,
setShippingAddress: setShippingAddressMock,
};
} ),
};
// Await here because the function returned by the __internalEmitPaymentProcessingEvent action creator
// (a thunk) returns a Promise.
await __internalEmitPaymentProcessingEvent(
currentObservers,
jest.fn()
)( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - it would be too much work to mock the entire registry, so we only mock dispatch on it,
// which is all we need to test this thunk.
registry: registryMock,
dispatch: {
...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ),
__internalSetPaymentMethodData: setPaymentMethodDataMock,
},
} );
expect( setBillingAddressMock ).toHaveBeenCalledWith(
testBillingAddress
);
expect( setShippingAddressMock ).toHaveBeenCalledWith(
testShippingAddress
);
expect( setPaymentMethodDataMock ).toHaveBeenCalledWith(
testPaymentMethodData
);
} );
it( 'sets metadata if failed observers return it', async () => {
const testFailingCallbackWithMetadata = jest.fn().mockReturnValue( {
type: 'failure',
meta: {
billingAddress: testBillingAddress,
paymentMethodData: testPaymentMethodData,
},
} );
currentObservers.payment_setup.set( 'test4', {
callback: testFailingCallbackWithMetadata,
priority: 10,
} );
const setBillingAddressMock = jest.fn();
const setPaymentMethodDataMock = jest.fn();
const registryMock = {
dispatch: jest.fn().mockImplementation( ( store: string ) => {
return {
...wpDataFunctions.dispatch( store ),
setBillingAddress: setBillingAddressMock,
};
} ),
};
// Await here because the function returned by the __internalEmitPaymentProcessingEvent action creator
// (a thunk) returns a Promise.
await __internalEmitPaymentProcessingEvent(
currentObservers,
jest.fn()
)( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - it would be too much work to mock the entire registry, so we only mock dispatch on it,
// which is all we need to test this thunk.
registry: registryMock,
dispatch: {
...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ),
__internalSetPaymentMethodData: setPaymentMethodDataMock,
},
} );
expect( setBillingAddressMock ).toHaveBeenCalledWith(
testBillingAddress
);
expect( setPaymentMethodDataMock ).toHaveBeenCalledWith(
testPaymentMethodData
);
} );
it( 'sets payment status to error if one observer is successful, but another errors', async () => {
const testErrorCallbackWithMetadata = jest
.fn()
.mockImplementation( () => {
return {
type: 'error',
};
} );
const testSuccessCallback = jest.fn().mockReturnValue( {
type: 'success',
} );
currentObservers.payment_setup.set( 'test5', {
callback: testErrorCallbackWithMetadata,
priority: 10,
} );
currentObservers.payment_setup.set( 'test6', {
callback: testSuccessCallback,
priority: 9,
} );
const setPaymentErrorMock = jest.fn();
const setPaymentReadyMock = jest.fn();
const registryMock = {
dispatch: jest
.fn()
.mockImplementation( wpDataFunctions.dispatch ),
};
// Await here because the function returned by the __internalEmitPaymentProcessingEvent action creator
// (a thunk) returns a Promise.
await __internalEmitPaymentProcessingEvent(
currentObservers,
jest.fn()
)( {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - it would be too much work to mock the entire registry, so we only mock dispatch on it,
// which is all we need to test this thunk.
registry: registryMock,
dispatch: {
...wpDataFunctions.dispatch( PAYMENT_STORE_KEY ),
__internalSetPaymentError: setPaymentErrorMock,
__internalSetPaymentReady: setPaymentReadyMock,
},
} );
// The observer throwing will cause this.
//expect( console ).toHaveErroredWith( new Error( 'test error' ) );
expect( setPaymentErrorMock ).toHaveBeenCalled();
expect( setPaymentReadyMock ).not.toHaveBeenCalled();
} );
} );
} );

View File

@@ -0,0 +1,215 @@
/**
* External dependencies
*/
import { store as noticesStore } from '@wordpress/notices';
import deprecated from '@wordpress/deprecated';
import type { BillingAddress, ShippingAddress } from '@woocommerce/settings';
import { isObject, isString, objectHasProp } from '@woocommerce/types';
/**
* Internal dependencies
*/
import {
emitEventWithAbort,
isErrorResponse,
isFailResponse,
isSuccessResponse,
noticeContexts,
ObserverResponse,
} from '../../base/context/event-emit';
import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit';
import type { emitProcessingEventType } from './types';
import { CART_STORE_KEY } from '../cart';
import {
isBillingAddress,
isShippingAddress,
} from '../../types/type-guards/address';
import { isObserverResponse } from '../../types/type-guards/observers';
import { isValidValidationErrorsObject } from '../../types/type-guards/validation';
export const __internalSetExpressPaymentError = ( message?: string ) => {
return ( { registry } ) => {
const { createErrorNotice, removeNotice } =
registry.dispatch( noticesStore );
if ( message ) {
createErrorNotice( message, {
id: 'wc-express-payment-error',
context: noticeContexts.EXPRESS_PAYMENTS,
} );
} else {
removeNotice(
'wc-express-payment-error',
noticeContexts.EXPRESS_PAYMENTS
);
}
};
};
/**
* Emit the payment_processing event
*/
export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
currentObserver,
setValidationErrors
) => {
return ( { dispatch, registry } ) => {
const { createErrorNotice, removeNotice } =
registry.dispatch( 'core/notices' );
removeNotice( 'wc-payment-error', noticeContexts.PAYMENTS );
return emitEventWithAbort(
currentObserver,
EMIT_TYPES.PAYMENT_SETUP,
{}
).then( ( observerResponses ) => {
let successResponse: ObserverResponse | undefined,
errorResponse: ObserverResponse | undefined,
billingAddress: BillingAddress | undefined,
shippingAddress: ShippingAddress | undefined;
observerResponses.forEach( ( response ) => {
if ( isSuccessResponse( response ) ) {
// The last observer response always "wins" for success.
successResponse = response;
}
// We consider both failed and error responses as an error.
if (
isErrorResponse( response ) ||
isFailResponse( response )
) {
errorResponse = response;
}
// Extensions may return shippingData, shippingAddress, billingData, and billingAddress in the response,
// so we need to check for all. If we detect either shippingData or billingData we need to show a
// deprecated warning for it, but also apply the changes to the wc/store/cart store.
const {
billingAddress: billingAddressFromResponse,
// Deprecated, but keeping it for now, for compatibility with extensions returning it.
billingData: billingDataFromResponse,
shippingAddress: shippingAddressFromResponse,
// Deprecated, but keeping it for now, for compatibility with extensions returning it.
shippingData: shippingDataFromResponse,
} = response?.meta || {};
billingAddress = billingAddressFromResponse as BillingAddress;
shippingAddress =
shippingAddressFromResponse as ShippingAddress;
if ( billingDataFromResponse ) {
// Set this here so that old extensions still using billingData can set the billingAddress.
billingAddress = billingDataFromResponse as BillingAddress;
deprecated(
'returning billingData from an onPaymentProcessing observer in WooCommerce Blocks',
{
version: '9.5.0',
alternative: 'billingAddress',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/6369',
}
);
}
if (
objectHasProp( shippingDataFromResponse, 'address' ) &&
shippingDataFromResponse.address
) {
// Set this here so that old extensions still using shippingData can set the shippingAddress.
shippingAddress =
shippingDataFromResponse.address as ShippingAddress;
deprecated(
'returning shippingData from an onPaymentProcessing observer in WooCommerce Blocks',
{
version: '9.5.0',
alternative: 'shippingAddress',
link: 'https://github.com/woocommerce/woocommerce-blocks/pull/8163',
}
);
}
} );
const { setBillingAddress, setShippingAddress } =
registry.dispatch( CART_STORE_KEY );
// Observer returned success, we sync the payment method data and billing address.
if ( isObserverResponse( successResponse ) && ! errorResponse ) {
const { paymentMethodData } = successResponse?.meta || {};
if ( isBillingAddress( billingAddress ) ) {
setBillingAddress( billingAddress );
}
if ( isShippingAddress( shippingAddress ) ) {
setShippingAddress( shippingAddress );
}
dispatch.__internalSetPaymentMethodData(
isObject( paymentMethodData ) ? paymentMethodData : {}
);
dispatch.__internalSetPaymentReady();
} else if ( isFailResponse( errorResponse ) ) {
const { paymentMethodData } = errorResponse?.meta || {};
if (
objectHasProp( errorResponse, 'message' ) &&
isString( errorResponse.message ) &&
errorResponse.message.length
) {
let context: string = noticeContexts.PAYMENTS;
if (
objectHasProp( errorResponse, 'messageContext' ) &&
isString( errorResponse.messageContext ) &&
errorResponse.messageContext.length
) {
context = errorResponse.messageContext;
}
createErrorNotice( errorResponse.message, {
id: 'wc-payment-error',
isDismissible: false,
context,
} );
}
if ( isBillingAddress( billingAddress ) ) {
setBillingAddress( billingAddress );
}
dispatch.__internalSetPaymentMethodData(
isObject( paymentMethodData ) ? paymentMethodData : {}
);
dispatch.__internalSetPaymentError();
} else if ( isErrorResponse( errorResponse ) ) {
if (
objectHasProp( errorResponse, 'message' ) &&
isString( errorResponse.message ) &&
errorResponse.message.length
) {
let context: string = noticeContexts.PAYMENTS;
if (
objectHasProp( errorResponse, 'messageContext' ) &&
isString( errorResponse.messageContext ) &&
errorResponse.messageContext.length
) {
context = errorResponse.messageContext;
}
createErrorNotice( errorResponse.message, {
id: 'wc-payment-error',
isDismissible: false,
context,
} );
}
dispatch.__internalSetPaymentError();
if (
isValidValidationErrorsObject(
errorResponse.validationErrors
)
) {
setValidationErrors( errorResponse.validationErrors );
}
} else {
// Otherwise there are no payment methods doing anything so just assume payment method is ready.
dispatch.__internalSetPaymentReady();
}
} );
};
};

View File

@@ -0,0 +1,96 @@
/**
* External dependencies
*/
import {
PlainPaymentMethods,
PlainExpressPaymentMethods,
} from '@woocommerce/types';
import type {
EmptyObjectType,
ObjectType,
FieldValidationStatus,
} from '@woocommerce/types';
import { DataRegistry } from '@wordpress/data';
/**
* Internal dependencies
*/
import type { EventObserversType } from '../../base/context/event-emit';
import type { DispatchFromMap } from '../mapped-types';
import * as actions from './actions';
export interface CustomerPaymentMethodConfiguration {
gateway: string;
brand: string;
last4: string;
}
export interface SavedPaymentMethod {
method: CustomerPaymentMethodConfiguration;
expires: string;
is_default: boolean;
tokenId: number;
actions: ObjectType;
}
export type SavedPaymentMethods =
| Record< string, SavedPaymentMethod[] >
| EmptyObjectType;
export interface PaymentMethodDispatchers {
setRegisteredPaymentMethods: (
paymentMethods: PlainPaymentMethods
) => void;
setRegisteredExpressPaymentMethods: (
paymentMethods: PlainExpressPaymentMethods
) => void;
setActivePaymentMethod: (
paymentMethod: string,
paymentMethodData?: ObjectType | EmptyObjectType
) => void;
}
export interface PaymentStatusDispatchers {
pristine: () => void;
started: () => void;
processing: () => void;
error: ( error: string ) => void;
failed: (
error?: string,
paymentMethodData?: ObjectType | EmptyObjectType,
billingAddress?: ObjectType | EmptyObjectType
) => void;
success: (
paymentMethodData?: ObjectType | EmptyObjectType,
billingAddress?: ObjectType | EmptyObjectType,
shippingData?: ObjectType | EmptyObjectType
) => void;
}
export type PaymentMethodsDispatcherType = (
paymentMethods: PlainPaymentMethods
) => undefined | void;
/**
* Type for emitProcessingEventType() thunk
*/
export type emitProcessingEventType = (
observers: EventObserversType,
setValidationErrors: (
errors: Record< string, FieldValidationStatus >
) => void
) => ( {
dispatch,
registry,
}: {
dispatch: DispatchFromMap< typeof actions >;
registry: DataRegistry;
} ) => void;
export interface PaymentStatus {
isPristine?: boolean;
isStarted?: boolean;
isProcessing?: boolean;
isFinished?: boolean;
hasError?: boolean;
hasFailed?: boolean;
isSuccessful?: boolean;
}

View File

@@ -0,0 +1,247 @@
/**
* External dependencies
*/
import {
CanMakePaymentArgument,
ExpressPaymentMethodConfigInstance,
PaymentMethodConfigInstance,
} from '@woocommerce/types';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
import { dispatch, select } from '@wordpress/data';
import {
deriveSelectedShippingRates,
emptyHiddenAddressFields,
} from '@woocommerce/base-utils';
import { __, sprintf } from '@wordpress/i18n';
import {
getExpressPaymentMethods,
getPaymentMethods,
} from '@woocommerce/blocks-registry';
import { previewCart } from '@woocommerce/resource-previews';
/**
* Internal dependencies
*/
import { STORE_KEY as CART_STORE_KEY } from '../../cart/constants';
import { STORE_KEY as PAYMENT_STORE_KEY } from '../constants';
import { noticeContexts } from '../../../base/context/event-emit';
import {
EMPTY_CART_ERRORS,
EMPTY_CART_ITEM_ERRORS,
EMPTY_EXTENSIONS,
} from '../../../data/constants';
import { defaultCartState } from '../../../data/cart/default-state';
/**
* Get the argument that will be passed to a payment method's `canMakePayment` method.
*/
export const getCanMakePaymentArg = (): CanMakePaymentArgument => {
const isEditor = !! select( 'core/editor' );
let canPayArgument: CanMakePaymentArgument;
if ( ! isEditor ) {
const store = select( CART_STORE_KEY );
const cart = store.getCartData();
const cartErrors = store.getCartErrors();
const cartTotals = store.getCartTotals();
const cartIsLoading = ! store.hasFinishedResolution( 'getCartData' );
const isLoadingRates = store.isCustomerDataUpdating();
const selectedShippingMethods = deriveSelectedShippingRates(
cart.shippingRates
);
const cartForCanPayArgument = {
cartCoupons: cart.coupons,
cartItems: cart.items,
crossSellsProducts: cart.crossSells,
cartFees: cart.fees,
cartItemsCount: cart.itemsCount,
cartItemsWeight: cart.itemsWeight,
cartNeedsPayment: cart.needsPayment,
cartNeedsShipping: cart.needsShipping,
cartItemErrors: cart.errors,
cartTotals,
cartIsLoading,
cartErrors,
billingData: emptyHiddenAddressFields( cart.billingAddress ),
billingAddress: emptyHiddenAddressFields( cart.billingAddress ),
shippingAddress: emptyHiddenAddressFields( cart.shippingAddress ),
extensions: cart.extensions,
shippingRates: cart.shippingRates,
isLoadingRates,
cartHasCalculatedShipping: cart.hasCalculatedShipping,
paymentRequirements: cart.paymentRequirements,
receiveCart: dispatch( CART_STORE_KEY ).receiveCart,
};
canPayArgument = {
cart: cartForCanPayArgument,
cartTotals: cart.totals,
cartNeedsShipping: cart.needsShipping,
billingData: cart.billingAddress,
billingAddress: cart.billingAddress,
shippingAddress: cart.shippingAddress,
selectedShippingMethods,
paymentMethods: cart.paymentMethods,
paymentRequirements: cart.paymentRequirements,
};
} else {
const cartForCanPayArgument = {
cartCoupons: previewCart.coupons,
cartItems: previewCart.items,
crossSellsProducts: previewCart.cross_sells,
cartFees: previewCart.fees,
cartItemsCount: previewCart.items_count,
cartItemsWeight: previewCart.items_weight,
cartNeedsPayment: previewCart.needs_payment,
cartNeedsShipping: previewCart.needs_shipping,
cartItemErrors: EMPTY_CART_ITEM_ERRORS,
cartTotals: previewCart.totals,
cartIsLoading: false,
cartErrors: EMPTY_CART_ERRORS,
billingData: defaultCartState.cartData.billingAddress,
billingAddress: defaultCartState.cartData.billingAddress,
shippingAddress: defaultCartState.cartData.shippingAddress,
extensions: EMPTY_EXTENSIONS,
shippingRates: previewCart.shipping_rates,
isLoadingRates: false,
cartHasCalculatedShipping: previewCart.has_calculated_shipping,
paymentRequirements: previewCart.payment_requirements,
receiveCart: () => undefined,
};
canPayArgument = {
cart: cartForCanPayArgument,
cartTotals: cartForCanPayArgument.cartTotals,
cartNeedsShipping: cartForCanPayArgument.cartNeedsShipping,
billingData: cartForCanPayArgument.billingAddress,
billingAddress: cartForCanPayArgument.billingAddress,
shippingAddress: cartForCanPayArgument.shippingAddress,
selectedShippingMethods: deriveSelectedShippingRates(
cartForCanPayArgument.shippingRates
),
paymentMethods: previewCart.payment_methods,
paymentRequirements: cartForCanPayArgument.paymentRequirements,
};
}
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( '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 ];
const paymentMethod = paymentMethods[ paymentMethodName ];
if ( ! paymentMethod ) {
continue;
}
// See if payment method should be available. This always evaluates to true in the editor context.
try {
const validForCart =
isEditor || express
? true
: cartPaymentMethods.includes( paymentMethodName );
const canPay = isEditor
? true
: validForCart &&
( await Promise.resolve(
paymentMethod.canMakePayment( canPayArgument )
) );
if ( canPay ) {
if ( typeof canPay === 'object' && canPay.error ) {
throw new Error( canPay.error.message );
}
addAvailablePaymentMethod( paymentMethod );
}
} catch ( e ) {
if ( CURRENT_USER_IS_ADMIN || isEditor ) {
registrationErrorNotice( paymentMethod, e as string, express );
}
}
}
const availablePaymentMethodNames = Object.keys( availablePaymentMethods );
const currentlyAvailablePaymentMethods = express
? select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods()
: select( PAYMENT_STORE_KEY ).getAvailablePaymentMethods();
if (
Object.keys( currentlyAvailablePaymentMethods ).length ===
availablePaymentMethodNames.length &&
Object.keys( currentlyAvailablePaymentMethods ).every( ( current ) =>
availablePaymentMethodNames.includes( current )
)
) {
// All the names are the same, no need to dispatch more actions.
return true;
}
const {
__internalSetAvailablePaymentMethods,
__internalSetAvailableExpressPaymentMethods,
} = dispatch( PAYMENT_STORE_KEY );
const setCallback = express
? __internalSetAvailableExpressPaymentMethods
: __internalSetAvailablePaymentMethods;
setCallback( availablePaymentMethods );
return true;
};

View File

@@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { getPaymentMethods } from '@woocommerce/blocks-registry';
/**
* Internal dependencies
*/
import type { SavedPaymentMethods } from '../types';
/**
* Gets the payment methods saved for the current user after filtering out disabled ones.
*/
export const filterActiveSavedPaymentMethods = (
availablePaymentMethods: string[] = [],
savedPaymentMethods: SavedPaymentMethods
): SavedPaymentMethods => {
if ( availablePaymentMethods.length === 0 ) {
return {};
}
const registeredPaymentMethods = getPaymentMethods();
const availablePaymentMethodsWithConfig = Object.fromEntries(
availablePaymentMethods.map( ( name ) => [
name,
registeredPaymentMethods[ name ],
] )
);
const paymentMethodKeys = Object.keys( savedPaymentMethods );
const activeSavedPaymentMethods = {} as SavedPaymentMethods;
paymentMethodKeys.forEach( ( type ) => {
const methods = savedPaymentMethods[ type ].filter(
( {
method: { gateway },
}: {
method: {
gateway: string;
};
} ) =>
gateway in availablePaymentMethodsWithConfig &&
availablePaymentMethodsWithConfig[ gateway ].supports
?.showSavedCards
);
if ( methods.length ) {
activeSavedPaymentMethods[ type ] = methods;
}
} );
return activeSavedPaymentMethods;
};

View File

@@ -0,0 +1,68 @@
/**
* External dependencies
*/
import { select, dispatch } from '@wordpress/data';
import { PlainPaymentMethods } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { STORE_KEY as PAYMENT_STORE_KEY } from '../constants';
export const setDefaultPaymentMethod = async (
paymentMethods: PlainPaymentMethods
) => {
const paymentMethodKeys = Object.keys( paymentMethods );
const expressPaymentMethodKeys = Object.keys(
select( PAYMENT_STORE_KEY ).getAvailableExpressPaymentMethods()
);
const allPaymentMethodKeys = [
...paymentMethodKeys,
...expressPaymentMethodKeys,
];
const savedPaymentMethods =
select( PAYMENT_STORE_KEY ).getSavedPaymentMethods();
const savedPaymentMethod =
Object.keys( savedPaymentMethods ).flatMap(
( type ) => savedPaymentMethods[ type ]
)[ 0 ] || undefined;
if ( savedPaymentMethod ) {
const token = savedPaymentMethod.tokenId.toString();
const paymentMethodSlug = savedPaymentMethod.method.gateway;
const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
dispatch( PAYMENT_STORE_KEY ).__internalSetActivePaymentMethod(
paymentMethodSlug,
{
token,
payment_method: paymentMethodSlug,
[ savedTokenKey ]: token,
isSavedToken: true,
}
);
return;
}
const activePaymentMethod =
select( PAYMENT_STORE_KEY ).getActivePaymentMethod();
// Return if current method is valid.
if (
activePaymentMethod &&
allPaymentMethodKeys.includes( activePaymentMethod )
) {
return;
}
dispatch( PAYMENT_STORE_KEY ).__internalSetPaymentIdle();
dispatch( PAYMENT_STORE_KEY ).__internalSetActivePaymentMethod(
paymentMethodKeys[ 0 ]
);
};