initial commit
This commit is contained in:
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { registeredBlockComponents } from './registered-block-components-init';
|
||||
|
||||
/**
|
||||
* Get all Registered Block Components.
|
||||
*
|
||||
* WooCommerce Blocks allows React Components to be used on the frontend of the store in place of
|
||||
* Blocks instead of just serving static content.
|
||||
*
|
||||
* This gets all registered Block Components so we know which Blocks map to which React Components.
|
||||
*
|
||||
* @param {string} context Current context (a named parent Block). If Block Components were only
|
||||
* registered under a certain context, those Components will be returned,
|
||||
* as well as any Components registered under all contexts.
|
||||
* @return {Object} List of React Components registered under the provided context.
|
||||
*/
|
||||
export function getRegisteredBlockComponents( context ) {
|
||||
const parentInnerBlocks =
|
||||
typeof registeredBlockComponents[ context ] === 'object' &&
|
||||
Object.keys( registeredBlockComponents[ context ] ).length > 0
|
||||
? registeredBlockComponents[ context ]
|
||||
: {};
|
||||
|
||||
return {
|
||||
...parentInnerBlocks,
|
||||
...registeredBlockComponents.any,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of getRegisteredBlockComponents kept for backwards compatibility.
|
||||
*
|
||||
* @param {string} main Name of the parent block to retrieve children of.
|
||||
* @return {Object} List of registered inner blocks.
|
||||
*/
|
||||
export function getRegisteredInnerBlocks( main ) {
|
||||
deprecated( 'getRegisteredInnerBlocks', {
|
||||
version: '2.8.0',
|
||||
alternative: 'getRegisteredBlockComponents',
|
||||
plugin: 'WooCommerce Blocks',
|
||||
} );
|
||||
return getRegisteredBlockComponents( main );
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './get-registered-block-components';
|
||||
export * from './register-block-component';
|
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { registeredBlockComponents } from './registered-block-components-init';
|
||||
|
||||
/**
|
||||
* Register a Block Component.
|
||||
*
|
||||
* WooCommerce Blocks allows React Components to be used on the frontend of the store in place of
|
||||
* Blocks instead of just serving static content.
|
||||
*
|
||||
* Registering a Block Component allows you to define which React Component should be used in place
|
||||
* of a registered Block. The Component, when rendered, will be passed all Block Attributes.
|
||||
*
|
||||
* @param {Object} options Options to use when registering the block.
|
||||
* @param {Function} options.component React component that will be rendered, or the return value from React.lazy if
|
||||
* dynamically imported.
|
||||
* @param {string} options.blockName Name of the block that this component belongs to.
|
||||
* @param {string} [options.context] To make this component available only under a certain context
|
||||
* (named parent Block) define it here. If left blank, the
|
||||
* Component will be available for all contexts.
|
||||
*/
|
||||
export function registerBlockComponent( options ) {
|
||||
if ( ! options.context ) {
|
||||
options.context = 'any';
|
||||
}
|
||||
assertOption( options, 'context', 'string' );
|
||||
assertOption( options, 'blockName', 'string' );
|
||||
assertBlockComponent( options, 'component' );
|
||||
|
||||
const { context, blockName, component } = options;
|
||||
|
||||
if ( ! registeredBlockComponents[ context ] ) {
|
||||
registeredBlockComponents[ context ] = {};
|
||||
}
|
||||
|
||||
registeredBlockComponents[ context ][ blockName ] = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that an option is a valid react element or lazy callback. Otherwise, throws an error.
|
||||
*
|
||||
* @throws Will throw an error if the type of the option doesn't match the expected type.
|
||||
* @param {Object} options Object containing the option to validate.
|
||||
* @param {string} optionName Name of the option to validate.
|
||||
*/
|
||||
const assertBlockComponent = ( options, optionName ) => {
|
||||
if ( options[ optionName ] ) {
|
||||
if ( typeof options[ optionName ] === 'function' ) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
options[ optionName ].$$typeof &&
|
||||
options[ optionName ].$$typeof === Symbol.for( 'react.lazy' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Incorrect value for the ${ optionName } argument when registering a block component. Component must be a valid React Element or Lazy callback.`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts that an option is of the given type. Otherwise, throws an error.
|
||||
*
|
||||
* @throws Will throw an error if the type of the option doesn't match the expected type.
|
||||
* @param {Object} options Object containing the option to validate.
|
||||
* @param {string} optionName Name of the option to validate.
|
||||
* @param {string} expectedType Type expected for the option.
|
||||
*/
|
||||
const assertOption = ( options, optionName, expectedType ) => {
|
||||
const actualType = typeof options[ optionName ];
|
||||
if ( actualType !== expectedType ) {
|
||||
throw new Error(
|
||||
`Incorrect value for the ${ optionName } argument when registering a block component. It was a ${ actualType }, but must be a ${ expectedType }.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Alias of registerBlockComponent kept for backwards compatibility.
|
||||
*
|
||||
* @param {Object} options Options to use when registering the block.
|
||||
* @param {string} options.main Name of the parent block.
|
||||
* @param {string} options.blockName Name of the child block being registered.
|
||||
* @param {Function} options.component React component used to render the child block.
|
||||
*/
|
||||
export function registerInnerBlock( options ) {
|
||||
deprecated( 'registerInnerBlock', {
|
||||
version: '2.8.0',
|
||||
alternative: 'registerBlockComponent',
|
||||
plugin: 'WooCommerce Blocks',
|
||||
hint: '"main" has been replaced with "context" and is now optional.',
|
||||
} );
|
||||
assertOption( options, 'main', 'string' );
|
||||
registerBlockComponent( {
|
||||
...options,
|
||||
context: options.main,
|
||||
} );
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
const registeredBlockComponents = {};
|
||||
|
||||
export { registeredBlockComponents };
|
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
getRegisteredBlockComponents,
|
||||
registerBlockComponent,
|
||||
registerInnerBlock,
|
||||
getRegisteredInnerBlocks,
|
||||
} from '../index';
|
||||
|
||||
describe( 'blocks registry', () => {
|
||||
const context = '@woocommerce/all-products';
|
||||
const blockName = '@woocommerce-extension/price-level';
|
||||
const component = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
describe( 'registerBlockComponent', () => {
|
||||
const invokeTest = ( args ) => () => {
|
||||
return registerBlockComponent( args );
|
||||
};
|
||||
it( 'throws an error when registered block is missing `blockName`', () => {
|
||||
expect( invokeTest( { context, blockName: null } ) ).toThrowError(
|
||||
/blockName/
|
||||
);
|
||||
} );
|
||||
it( 'throws an error when registered block is missing `component`', () => {
|
||||
expect(
|
||||
invokeTest( { context, blockName, component: null } )
|
||||
).toThrowError( /component/ );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getRegisteredBlockComponents', () => {
|
||||
it( 'gets an empty object when context has no inner blocks', () => {
|
||||
expect(
|
||||
getRegisteredBlockComponents( '@woocommerce/all-products' )
|
||||
).toEqual( {} );
|
||||
} );
|
||||
it( 'gets a block that was successfully registered', () => {
|
||||
registerBlockComponent( { context, blockName, component } );
|
||||
expect(
|
||||
getRegisteredBlockComponents( '@woocommerce/all-products' )
|
||||
).toEqual( { [ blockName ]: component } );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'registerInnerBlock (deprecated)', () => {
|
||||
const invokeTest = ( args ) => () => {
|
||||
registerInnerBlock( args );
|
||||
};
|
||||
|
||||
it( 'throws an error when registered block is missing `main`', () => {
|
||||
const options = { main: null };
|
||||
expect( invokeTest( options ) ).toThrowError( /main/ );
|
||||
expect( console ).toHaveWarned();
|
||||
} );
|
||||
it( 'throws an error when registered block is missing `blockName`', () => {
|
||||
const options = { main: context, blockName: null };
|
||||
expect( invokeTest( options ) ).toThrowError( /blockName/ );
|
||||
} );
|
||||
it( 'throws an error when registered block is missing `component`', () => {
|
||||
const options = { main: context, blockName, component: null };
|
||||
expect( invokeTest( options ) ).toThrowError( /component/ );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getRegisteredInnerBlocks (deprecated)', () => {
|
||||
it( 'gets an empty object when parent has no inner blocks', () => {
|
||||
expect(
|
||||
getRegisteredInnerBlocks( '@woocommerce/test-parent' )
|
||||
).toEqual( {} );
|
||||
expect( console ).toHaveWarned();
|
||||
} );
|
||||
it( 'gets a block that was successfully registered', () => {
|
||||
registerBlockComponent( { context, blockName, component } );
|
||||
expect(
|
||||
getRegisteredInnerBlocks( '@woocommerce/all-products' )
|
||||
).toEqual( {
|
||||
[ blockName ]: component,
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,2 @@
|
||||
export * from './payment-methods';
|
||||
export * from './block-components';
|
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isValidElement } from '@wordpress/element';
|
||||
import type { ReactNode } from 'react';
|
||||
import type {
|
||||
PaymentMethodConfiguration,
|
||||
ExpressPaymentMethodConfiguration,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
|
||||
export const assertValidPaymentMethodComponent = (
|
||||
component: () => unknown,
|
||||
componentName: string
|
||||
): void => {
|
||||
if ( typeof component !== 'function' ) {
|
||||
throw new TypeError(
|
||||
`The ${ componentName } property for the payment method must be a functional component`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const assertValidElement = (
|
||||
element: ReactNode,
|
||||
elementName: string
|
||||
): void => {
|
||||
if ( element !== null && ! isValidElement( element ) ) {
|
||||
throw new TypeError(
|
||||
`The ${ elementName } property for the payment method must be a React element or null.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const assertValidElementOrString = (
|
||||
element: ReactNode,
|
||||
elementName: string
|
||||
): void => {
|
||||
if (
|
||||
element !== null &&
|
||||
! isValidElement( element ) &&
|
||||
typeof element !== 'string'
|
||||
) {
|
||||
throw new TypeError(
|
||||
`The ${ elementName } property for the payment method must be a React element, a string, or null.`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const assertConfigHasProperties = (
|
||||
config: ExpressPaymentMethodConfiguration | PaymentMethodConfiguration,
|
||||
expectedProperties: string[] = []
|
||||
): void => {
|
||||
const missingProperties = expectedProperties.reduce(
|
||||
( acc: string[], property: string ) => {
|
||||
if ( ! config.hasOwnProperty( property ) ) {
|
||||
acc.push( property );
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
if ( missingProperties.length > 0 ) {
|
||||
const message =
|
||||
'The payment method configuration object is missing the following properties:';
|
||||
throw new TypeError( message + missingProperties.join( ', ' ) );
|
||||
}
|
||||
};
|
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import type {
|
||||
ExpressPaymentMethodConfiguration,
|
||||
Supports,
|
||||
CanMakePaymentCallback,
|
||||
ExpressPaymentMethodConfigInstance,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getCanMakePayment } from './payment-method-config-helper';
|
||||
import { assertConfigHasProperties, assertValidElement } from './assertions';
|
||||
|
||||
export default class ExpressPaymentMethodConfig
|
||||
implements ExpressPaymentMethodConfigInstance {
|
||||
public name: string;
|
||||
public content: ReactNode;
|
||||
public edit: ReactNode;
|
||||
public paymentMethodId?: string;
|
||||
public supports: Supports;
|
||||
public canMakePaymentFromConfig: CanMakePaymentCallback;
|
||||
|
||||
constructor( config: ExpressPaymentMethodConfiguration ) {
|
||||
// validate config
|
||||
ExpressPaymentMethodConfig.assertValidConfig( config );
|
||||
this.name = config.name;
|
||||
this.content = config.content;
|
||||
this.edit = config.edit;
|
||||
this.paymentMethodId = config.paymentMethodId || this.name;
|
||||
this.supports = {
|
||||
features: config?.supports?.features || [ 'products' ],
|
||||
};
|
||||
this.canMakePaymentFromConfig = config.canMakePayment;
|
||||
}
|
||||
|
||||
// canMakePayment is calculated each time based on data that modifies outside of the class (eg: cart data).
|
||||
get canMakePayment(): CanMakePaymentCallback {
|
||||
return getCanMakePayment(
|
||||
this.canMakePaymentFromConfig,
|
||||
this.supports.features,
|
||||
this.name
|
||||
);
|
||||
}
|
||||
|
||||
static assertValidConfig = (
|
||||
config: ExpressPaymentMethodConfiguration
|
||||
): void => {
|
||||
assertConfigHasProperties( config, [ 'name', 'content', 'edit' ] );
|
||||
if ( typeof config.name !== 'string' ) {
|
||||
throw new TypeError(
|
||||
'The name property for the express payment method must be a string'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.paymentMethodId !== 'string' &&
|
||||
typeof config.paymentMethodId !== 'undefined'
|
||||
) {
|
||||
throw new Error(
|
||||
'The paymentMethodId property for the payment method must be a string or undefined (in which case it will be the value of the name property).'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.supports?.features !== 'undefined' &&
|
||||
! Array.isArray( config.supports?.features )
|
||||
) {
|
||||
throw new Error(
|
||||
'The features property for the payment method must be an array or undefined.'
|
||||
);
|
||||
}
|
||||
assertValidElement( config.content, 'content' );
|
||||
assertValidElement( config.edit, 'edit' );
|
||||
if ( typeof config.canMakePayment !== 'function' ) {
|
||||
throw new TypeError(
|
||||
'The canMakePayment property for the express payment method must be a function.'
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CanMakePaymentExtensionCallback } from '@woocommerce/type-defs/payments';
|
||||
|
||||
type CanMakePaymentExtensionCallbacks = Record<
|
||||
string,
|
||||
CanMakePaymentExtensionCallback
|
||||
>;
|
||||
export type NamespacedCanMakePaymentExtensionsCallbacks = Record<
|
||||
string,
|
||||
CanMakePaymentExtensionCallbacks
|
||||
>;
|
||||
export type ExtensionNamespace = keyof NamespacedCanMakePaymentExtensionsCallbacks;
|
||||
export type PaymentMethodName = keyof CanMakePaymentExtensionCallbacks;
|
||||
|
||||
// Keeps callbacks registered by extensions for different payment methods
|
||||
// eslint-disable-next-line prefer-const
|
||||
export const canMakePaymentExtensionsCallbacks: NamespacedCanMakePaymentExtensionsCallbacks = {};
|
||||
|
||||
export const extensionsConfig = {
|
||||
canMakePayment: canMakePaymentExtensionsCallbacks,
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './registry';
|
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type {
|
||||
CanMakePaymentCallback,
|
||||
CanMakePaymentExtensionCallback,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
NamespacedCanMakePaymentExtensionsCallbacks,
|
||||
PaymentMethodName,
|
||||
ExtensionNamespace,
|
||||
extensionsConfig,
|
||||
} from './extensions-config';
|
||||
|
||||
// Filter out payment methods by supported features and cart requirement.
|
||||
export const canMakePaymentWithFeaturesCheck = (
|
||||
canMakePayment: CanMakePaymentCallback,
|
||||
features: string[]
|
||||
): CanMakePaymentCallback => ( canPayArgument ) => {
|
||||
const requirements = canPayArgument?.paymentRequirements || [];
|
||||
const featuresSupportRequirements = requirements.every( ( requirement ) =>
|
||||
features.includes( requirement )
|
||||
);
|
||||
return featuresSupportRequirements && canMakePayment( canPayArgument );
|
||||
};
|
||||
|
||||
// Filter out payment methods by callbacks registered by extensions.
|
||||
export const canMakePaymentWithExtensions = (
|
||||
canMakePayment: CanMakePaymentCallback,
|
||||
extensionsCallbacks: NamespacedCanMakePaymentExtensionsCallbacks,
|
||||
paymentMethodName: PaymentMethodName
|
||||
): CanMakePaymentCallback => ( canPayArgument ) => {
|
||||
// Validate whether the payment method is available based on its own criteria first.
|
||||
let canPay = canMakePayment( canPayArgument );
|
||||
|
||||
if ( canPay ) {
|
||||
// Gather all callbacks for paymentMethodName.
|
||||
const namespacedCallbacks: Record<
|
||||
ExtensionNamespace,
|
||||
CanMakePaymentExtensionCallback
|
||||
> = {};
|
||||
|
||||
Object.entries( extensionsCallbacks ).forEach(
|
||||
( [ namespace, callbacks ] ) => {
|
||||
namespacedCallbacks[ namespace ] =
|
||||
callbacks[ paymentMethodName ];
|
||||
}
|
||||
);
|
||||
|
||||
canPay = Object.keys( namespacedCallbacks ).every( ( namespace ) => {
|
||||
try {
|
||||
return namespacedCallbacks[ namespace ]( canPayArgument );
|
||||
} catch ( err ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Error when executing callback for ${ paymentMethodName } in ${ namespace }`,
|
||||
err
|
||||
);
|
||||
// .every() expects a return value at the end of every arrow function and
|
||||
// this ensures that the error is ignored when computing the whole result.
|
||||
return true;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return canPay;
|
||||
};
|
||||
|
||||
export const getCanMakePayment = (
|
||||
canMakePayment: CanMakePaymentCallback,
|
||||
features: string[],
|
||||
paymentMethodName: string
|
||||
): CanMakePaymentCallback => {
|
||||
const canPay = canMakePaymentWithFeaturesCheck( canMakePayment, features );
|
||||
// Loop through all callbacks to check if there are any registered for this payment method.
|
||||
return ( Object.values( extensionsConfig.canMakePayment ) as Record<
|
||||
PaymentMethodName,
|
||||
CanMakePaymentCallback
|
||||
>[] ).some( ( callbacks ) => paymentMethodName in callbacks )
|
||||
? canMakePaymentWithExtensions(
|
||||
canPay,
|
||||
extensionsConfig.canMakePayment,
|
||||
paymentMethodName
|
||||
)
|
||||
: canPay;
|
||||
};
|
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
import type { ReactNode } from 'react';
|
||||
import type {
|
||||
PaymentMethodConfiguration,
|
||||
Supports,
|
||||
CanMakePaymentCallback,
|
||||
PaymentMethodConfigInstance,
|
||||
PaymentMethodIcons,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getCanMakePayment } from './payment-method-config-helper';
|
||||
import {
|
||||
assertConfigHasProperties,
|
||||
assertValidElement,
|
||||
assertValidElementOrString,
|
||||
} from './assertions';
|
||||
|
||||
const NullComponent = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default class PaymentMethodConfig
|
||||
implements PaymentMethodConfigInstance {
|
||||
public name: string;
|
||||
public content: ReactNode;
|
||||
public edit: ReactNode;
|
||||
public paymentMethodId?: string;
|
||||
public supports: Supports;
|
||||
public icons: null | PaymentMethodIcons;
|
||||
public label: ReactNode;
|
||||
public ariaLabel: string;
|
||||
public placeOrderButtonLabel?: string;
|
||||
public savedTokenComponent?: ReactNode | null;
|
||||
public canMakePaymentFromConfig: CanMakePaymentCallback;
|
||||
|
||||
constructor( config: PaymentMethodConfiguration ) {
|
||||
// validate config
|
||||
PaymentMethodConfig.assertValidConfig( config );
|
||||
this.name = config.name;
|
||||
this.label = config.label;
|
||||
this.placeOrderButtonLabel = config.placeOrderButtonLabel;
|
||||
this.ariaLabel = config.ariaLabel;
|
||||
this.content = config.content;
|
||||
this.savedTokenComponent = config.savedTokenComponent;
|
||||
this.icons = config.icons || null;
|
||||
this.edit = config.edit;
|
||||
this.paymentMethodId = config.paymentMethodId || this.name;
|
||||
this.supports = {
|
||||
showSavedCards:
|
||||
config?.supports?.showSavedCards ||
|
||||
config?.supports?.savePaymentInfo || // Kept for backward compatibility if methods still pass this when registering.
|
||||
false,
|
||||
showSaveOption: config?.supports?.showSaveOption || false,
|
||||
features: config?.supports?.features || [ 'products' ],
|
||||
};
|
||||
this.canMakePaymentFromConfig = config.canMakePayment;
|
||||
}
|
||||
|
||||
// canMakePayment is calculated each time based on data that modifies outside of the class (eg: cart data).
|
||||
get canMakePayment(): CanMakePaymentCallback {
|
||||
return getCanMakePayment(
|
||||
this.canMakePaymentFromConfig,
|
||||
this.supports.features,
|
||||
this.name
|
||||
);
|
||||
}
|
||||
|
||||
static assertValidConfig = ( config: PaymentMethodConfiguration ): void => {
|
||||
// set default for optional
|
||||
config.savedTokenComponent = config.savedTokenComponent || (
|
||||
<NullComponent />
|
||||
);
|
||||
assertConfigHasProperties( config, [
|
||||
'name',
|
||||
'label',
|
||||
'ariaLabel',
|
||||
'content',
|
||||
'edit',
|
||||
'canMakePayment',
|
||||
] );
|
||||
if ( typeof config.name !== 'string' ) {
|
||||
throw new Error(
|
||||
'The name property for the payment method must be a string'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.icons !== 'undefined' &&
|
||||
! Array.isArray( config.icons ) &&
|
||||
config.icons !== null
|
||||
) {
|
||||
throw new Error(
|
||||
'The icons property for the payment method must be an array or null.'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.paymentMethodId !== 'string' &&
|
||||
typeof config.paymentMethodId !== 'undefined'
|
||||
) {
|
||||
throw new Error(
|
||||
'The paymentMethodId property for the payment method must be a string or undefined (in which case it will be the value of the name property).'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.placeOrderButtonLabel !== 'string' &&
|
||||
typeof config.placeOrderButtonLabel !== 'undefined'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'The placeOrderButtonLabel property for the payment method must be a string'
|
||||
);
|
||||
}
|
||||
assertValidElementOrString( config.label, 'label' );
|
||||
assertValidElement( config.content, 'content' );
|
||||
assertValidElement( config.edit, 'edit' );
|
||||
assertValidElement( config.savedTokenComponent, 'savedTokenComponent' );
|
||||
if ( typeof config.ariaLabel !== 'string' ) {
|
||||
throw new TypeError(
|
||||
'The ariaLabel property for the payment method must be a string'
|
||||
);
|
||||
}
|
||||
if ( typeof config.canMakePayment !== 'function' ) {
|
||||
throw new TypeError(
|
||||
'The canMakePayment property for the payment method must be a function.'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.supports?.showSavedCards !== 'undefined' &&
|
||||
typeof config.supports?.showSavedCards !== 'boolean'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'If the payment method includes the `supports.showSavedCards` property, it must be a boolean'
|
||||
);
|
||||
}
|
||||
if ( typeof config.supports?.savePaymentInfo !== 'undefined' ) {
|
||||
deprecated(
|
||||
'Passing savePaymentInfo when registering a payment method.',
|
||||
{
|
||||
alternative: 'Pass showSavedCards and showSaveOption',
|
||||
plugin: 'woocommerce-gutenberg-products-block',
|
||||
link:
|
||||
'https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3686',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.supports?.features !== 'undefined' &&
|
||||
! Array.isArray( config.supports?.features )
|
||||
) {
|
||||
throw new Error(
|
||||
'The features property for the payment method must be an array or undefined.'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof config.supports?.showSaveOption !== 'undefined' &&
|
||||
typeof config.supports?.showSaveOption !== 'boolean'
|
||||
) {
|
||||
throw new TypeError(
|
||||
'If the payment method includes the `supports.showSaveOption` property, it must be a boolean'
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
import type {
|
||||
PaymentMethodConfiguration,
|
||||
ExpressPaymentMethodConfiguration,
|
||||
CanMakePaymentExtensionCallback,
|
||||
PaymentMethodConfigInstance,
|
||||
PaymentMethods,
|
||||
ExpressPaymentMethods,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { default as PaymentMethodConfig } from './payment-method-config';
|
||||
import { default as ExpressPaymentMethodConfig } from './express-payment-method-config';
|
||||
import { canMakePaymentExtensionsCallbacks } from './extensions-config';
|
||||
|
||||
type LegacyRegisterPaymentMethodFunction = ( config: unknown ) => unknown;
|
||||
type LegacyRegisterExpessPaymentMethodFunction = ( config: unknown ) => unknown;
|
||||
|
||||
const paymentMethods: PaymentMethods = {};
|
||||
const expressPaymentMethods: ExpressPaymentMethods = {};
|
||||
|
||||
/**
|
||||
* Register a regular payment method.
|
||||
*/
|
||||
export const registerPaymentMethod = (
|
||||
options: PaymentMethodConfiguration | LegacyRegisterPaymentMethodFunction
|
||||
): void => {
|
||||
let paymentMethodConfig: PaymentMethodConfigInstance | unknown;
|
||||
if ( typeof options === 'function' ) {
|
||||
// Legacy fallback for previous API, where client passes a function:
|
||||
// registerPaymentMethod( ( Config ) => new Config( options ) );
|
||||
paymentMethodConfig = options( PaymentMethodConfig );
|
||||
deprecated( 'Passing a callback to registerPaymentMethod()', {
|
||||
alternative: 'a config options object',
|
||||
plugin: 'woocommerce-gutenberg-products-block',
|
||||
link:
|
||||
'https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3404',
|
||||
} );
|
||||
} else {
|
||||
paymentMethodConfig = new PaymentMethodConfig( options );
|
||||
}
|
||||
if ( paymentMethodConfig instanceof PaymentMethodConfig ) {
|
||||
paymentMethods[ paymentMethodConfig.name ] = paymentMethodConfig;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register an express payment method.
|
||||
*/
|
||||
export const registerExpressPaymentMethod = (
|
||||
options:
|
||||
| ExpressPaymentMethodConfiguration
|
||||
| LegacyRegisterExpessPaymentMethodFunction
|
||||
): void => {
|
||||
let paymentMethodConfig;
|
||||
if ( typeof options === 'function' ) {
|
||||
// Legacy fallback for previous API, where client passes a function:
|
||||
// registerExpressPaymentMethod( ( Config ) => new Config( options ) );
|
||||
paymentMethodConfig = options( ExpressPaymentMethodConfig );
|
||||
deprecated( 'Passing a callback to registerExpressPaymentMethod()', {
|
||||
alternative: 'a config options object',
|
||||
plugin: 'woocommerce-gutenberg-products-block',
|
||||
link:
|
||||
'https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3404',
|
||||
} );
|
||||
} else {
|
||||
paymentMethodConfig = new ExpressPaymentMethodConfig( options );
|
||||
}
|
||||
if ( paymentMethodConfig instanceof ExpressPaymentMethodConfig ) {
|
||||
expressPaymentMethods[ paymentMethodConfig.name ] = paymentMethodConfig;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows extension to register callbacks for specific payment methods to determine if they can make payments
|
||||
*/
|
||||
export const registerPaymentMethodExtensionCallbacks = (
|
||||
namespace: string,
|
||||
callbacks: Record< string, CanMakePaymentExtensionCallback >
|
||||
): void => {
|
||||
if ( canMakePaymentExtensionsCallbacks[ namespace ] ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`The namespace provided to registerPaymentMethodExtensionCallbacks must be unique. Callbacks have already been registered for the ${ namespace } namespace.`
|
||||
);
|
||||
} else {
|
||||
// Set namespace up as an empty object.
|
||||
canMakePaymentExtensionsCallbacks[ namespace ] = {};
|
||||
|
||||
Object.entries( callbacks ).forEach(
|
||||
( [ paymentMethodName, callback ] ) => {
|
||||
if ( typeof callback === 'function' ) {
|
||||
canMakePaymentExtensionsCallbacks[ namespace ][
|
||||
paymentMethodName
|
||||
] = callback;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`All callbacks provided to registerPaymentMethodExtensionCallbacks must be functions. The callback for the ${ paymentMethodName } payment method in the ${ namespace } namespace was not a function.`
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const __experimentalDeRegisterPaymentMethod = (
|
||||
paymentMethodName: string
|
||||
): void => {
|
||||
delete paymentMethods[ paymentMethodName ];
|
||||
};
|
||||
|
||||
export const __experimentalDeRegisterExpressPaymentMethod = (
|
||||
paymentMethodName: string
|
||||
): void => {
|
||||
delete expressPaymentMethods[ paymentMethodName ];
|
||||
};
|
||||
|
||||
export const getPaymentMethods = (): PaymentMethods => {
|
||||
return paymentMethods;
|
||||
};
|
||||
|
||||
export const getExpressPaymentMethods = (): ExpressPaymentMethods => {
|
||||
return expressPaymentMethods;
|
||||
};
|
@ -0,0 +1,205 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethodExtensionCallbacks } from '@woocommerce/blocks-registry';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import * as helpers from '../payment-method-config-helper';
|
||||
import { canMakePaymentExtensionsCallbacks } from '../extensions-config';
|
||||
|
||||
const canMakePaymentArgument = {
|
||||
cartTotals: {
|
||||
total_items: '1488',
|
||||
total_items_tax: '312',
|
||||
total_fees: '0',
|
||||
total_fees_tax: '0',
|
||||
total_discount: '0',
|
||||
total_discount_tax: '0',
|
||||
total_shipping: '0',
|
||||
total_shipping_tax: '0',
|
||||
total_price: '1800',
|
||||
total_tax: '312',
|
||||
tax_lines: [
|
||||
{
|
||||
name: 'BTW',
|
||||
price: '312',
|
||||
rate: '21%',
|
||||
},
|
||||
],
|
||||
currency_code: 'EUR',
|
||||
currency_symbol: '€',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: ',',
|
||||
currency_thousand_separator: '.',
|
||||
currency_prefix: '€',
|
||||
currency_suffix: '',
|
||||
},
|
||||
cartNeedsShipping: true,
|
||||
billingData: {
|
||||
first_name: 'name',
|
||||
last_name: 'Name',
|
||||
company: '',
|
||||
address_1: 'fdsfdsfdsf',
|
||||
address_2: '',
|
||||
city: 'Berlin',
|
||||
state: '',
|
||||
postcode: 'xxxxx',
|
||||
country: 'DE',
|
||||
email: 'name.Name@test.com',
|
||||
phone: '1234',
|
||||
},
|
||||
shippingAddress: {
|
||||
first_name: 'name',
|
||||
last_name: 'Name',
|
||||
company: '',
|
||||
address_1: 'fdsfdsfdsf',
|
||||
address_2: '',
|
||||
city: 'Berlin',
|
||||
state: '',
|
||||
postcode: 'xxxxx',
|
||||
country: 'DE',
|
||||
phone: '1234',
|
||||
},
|
||||
selectedShippingMethods: {
|
||||
'0': 'free_shipping:1',
|
||||
},
|
||||
paymentRequirements: [ 'products' ],
|
||||
};
|
||||
describe( 'payment-method-config-helper', () => {
|
||||
const trueCallback = jest.fn().mockReturnValue( true );
|
||||
const falseCallback = jest.fn().mockReturnValue( false );
|
||||
const bacsCallback = jest.fn().mockReturnValue( false );
|
||||
const throwsCallback = jest.fn().mockImplementation( () => {
|
||||
throw new Error();
|
||||
} );
|
||||
beforeAll( () => {
|
||||
// Register extension callbacks for two payment methods.
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'woocommerce-marketplace-extension',
|
||||
{
|
||||
// cod: one extension returns true, the other returns false.
|
||||
cod: trueCallback,
|
||||
// cheque: returns true only if arg.billingData.postcode is 12345.
|
||||
cheque: ( arg ) => arg.billingData.postcode === '12345',
|
||||
// bacs: both extensions return false.
|
||||
bacs: bacsCallback,
|
||||
// woopay: both extensions return true.
|
||||
woopay: trueCallback,
|
||||
// testpay: one callback errors, one returns true
|
||||
testpay: throwsCallback,
|
||||
}
|
||||
);
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'other-woocommerce-marketplace-extension',
|
||||
{
|
||||
cod: falseCallback,
|
||||
woopay: trueCallback,
|
||||
testpay: trueCallback,
|
||||
bacs: bacsCallback,
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
beforeEach( () => {
|
||||
trueCallback.mockClear();
|
||||
throwsCallback.mockClear();
|
||||
falseCallback.mockClear();
|
||||
bacsCallback.mockClear();
|
||||
} );
|
||||
describe( 'getCanMakePayment', () => {
|
||||
it( 'returns callback canMakePaymentWithFeaturesCheck if no extension callback is detected', () => {
|
||||
// Define arguments from a payment method ('missing-payment-method') with no registered extension callbacks.
|
||||
const args = {
|
||||
canMakePayment: jest.fn().mockImplementation( () => true ),
|
||||
features: [ 'products' ],
|
||||
paymentMethodName: 'missing-payment-method',
|
||||
};
|
||||
|
||||
const canMakePayment = helpers.getCanMakePayment(
|
||||
args.canMakePayment,
|
||||
args.features,
|
||||
args.paymentMethodName
|
||||
)( canMakePaymentArgument );
|
||||
|
||||
// Expect that the result of getCanMakePayment is the result of
|
||||
// the payment method's own canMakePayment, as no extension callbacks are called.
|
||||
expect( canMakePayment ).toEqual( args.canMakePayment() );
|
||||
} );
|
||||
|
||||
it( 'returns callbacks from the extensions when they are defined', () => {
|
||||
// Define arguments from a payment method (bacs) with registered extension callbacks.
|
||||
const args = {
|
||||
canMakePaymentConfiguration: jest
|
||||
.fn()
|
||||
.mockImplementation( () => true ),
|
||||
features: [ 'products' ],
|
||||
paymentMethodName: 'bacs',
|
||||
};
|
||||
|
||||
const canMakePayment = helpers.getCanMakePayment(
|
||||
args.canMakePaymentConfiguration,
|
||||
args.features,
|
||||
args.paymentMethodName
|
||||
)( canMakePaymentArgument );
|
||||
|
||||
// Expect that the result of getCanMakePayment is not the result of
|
||||
// the payment method's own canMakePayment (args.canMakePaymentConfiguration),
|
||||
// but of the registered bacsCallback.
|
||||
expect( canMakePayment ).toBe( bacsCallback() );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'canMakePaymentWithExtensions', () => {
|
||||
it( "Returns false without executing the registered callbacks, if the payment method's canMakePayment callback returns false.", () => {
|
||||
const canMakePayment = () => false;
|
||||
const canMakePaymentWithExtensionsResult = helpers.canMakePaymentWithExtensions(
|
||||
canMakePayment,
|
||||
canMakePaymentExtensionsCallbacks,
|
||||
'cod'
|
||||
)( canMakePaymentArgument );
|
||||
expect( canMakePaymentWithExtensionsResult ).toBe( false );
|
||||
expect( trueCallback ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'Returns early when a registered callback returns false, without executing all the registered callbacks', () => {
|
||||
helpers.canMakePaymentWithExtensions(
|
||||
() => true,
|
||||
canMakePaymentExtensionsCallbacks,
|
||||
'bacs'
|
||||
)( canMakePaymentArgument );
|
||||
expect( bacsCallback ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'Returns true if all extension callbacks return true', () => {
|
||||
const result = helpers.canMakePaymentWithExtensions(
|
||||
() => true,
|
||||
canMakePaymentExtensionsCallbacks,
|
||||
'woopay'
|
||||
)( canMakePaymentArgument );
|
||||
expect( result ).toBe( true );
|
||||
} );
|
||||
|
||||
it( 'Passes canPayArg to the callback', () => {
|
||||
helpers.canMakePaymentWithExtensions(
|
||||
() => true,
|
||||
canMakePaymentExtensionsCallbacks,
|
||||
'woopay'
|
||||
)( canMakePaymentArgument );
|
||||
expect( trueCallback ).toHaveBeenCalledWith(
|
||||
canMakePaymentArgument
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'Allows all valid callbacks to run, even if one causes an error', () => {
|
||||
helpers.canMakePaymentWithExtensions(
|
||||
() => true,
|
||||
canMakePaymentExtensionsCallbacks,
|
||||
'testpay'
|
||||
)( canMakePaymentArgument );
|
||||
expect( console ).toHaveErrored();
|
||||
expect( throwsCallback ).toHaveBeenCalledTimes( 1 );
|
||||
expect( trueCallback ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethodExtensionCallbacks } from '@woocommerce/blocks-registry';
|
||||
import type { PaymentMethodConfigInstance } from '@woocommerce/type-defs/payments';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import PaymentMethodConfig from '../payment-method-config';
|
||||
import * as paymentMethodConfigHelpers from '../payment-method-config-helper';
|
||||
|
||||
describe( 'PaymentMethodConfig', () => {
|
||||
let paymentMethod: PaymentMethodConfigInstance;
|
||||
const extensionsCallbackSpy = jest.spyOn(
|
||||
paymentMethodConfigHelpers,
|
||||
'canMakePaymentWithExtensions'
|
||||
);
|
||||
beforeEach( () => {
|
||||
paymentMethod = new PaymentMethodConfig( {
|
||||
name: 'test-payment-method',
|
||||
label: 'Test payment method',
|
||||
ariaLabel: 'Test payment method',
|
||||
content: <div>Test payment content</div>,
|
||||
edit: <div>Test payment edit</div>,
|
||||
canMakePayment: () => true,
|
||||
supports: { features: [ 'products' ] },
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'Uses canMakePaymentWithExtensions as the canMakePayment function if an extension registers a callback', () => {
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'woocommerce-marketplace-extension',
|
||||
{
|
||||
'unrelated-payment-method': () => true,
|
||||
}
|
||||
);
|
||||
|
||||
// At this point, since no extensions have registered a callback for
|
||||
// test-payment-method we can expect the canMakePayment getter NOT
|
||||
// to execute canMakePaymentWithExtensions.
|
||||
// Disable no-unused-expressions because we just want to test the getter
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
paymentMethod.canMakePayment;
|
||||
expect( extensionsCallbackSpy ).toHaveBeenCalledTimes( 0 );
|
||||
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'other-woocommerce-marketplace-extension',
|
||||
{
|
||||
'test-payment-method': () => true,
|
||||
}
|
||||
);
|
||||
|
||||
// Now, because an extension _has_ registered a callback for test-payment-method
|
||||
// The getter will use canMakePaymentWithExtensions to create the
|
||||
// canMakePayment function.
|
||||
// Disable no-unused-expressions because we just want to test the getter
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
paymentMethod.canMakePayment;
|
||||
expect( extensionsCallbackSpy ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerPaymentMethodExtensionCallbacks } from '@woocommerce/blocks-registry';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { canMakePaymentExtensionsCallbacks } from '../extensions-config';
|
||||
|
||||
describe( 'registerPaymentMethodExtensionCallbacks', () => {
|
||||
it( 'Logs an error to console if namespace is already registered', () => {
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'woocommerce-marketplace-extension',
|
||||
{
|
||||
cod: () => false,
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect( console ).not.toHaveErrored();
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'woocommerce-marketplace-extension',
|
||||
{
|
||||
cod: () => false,
|
||||
}
|
||||
);
|
||||
expect( console ).toHaveErrored();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect( console.error ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'Does not overwrite a namespace if a second extensions tries to register with the same name', () => {
|
||||
const firstCodCallback = jest.fn().mockReturnValue( false );
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'overwrite-marketplace-extension',
|
||||
{
|
||||
cod: firstCodCallback,
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect( console ).not.toHaveErrored();
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'overwrite-marketplace-extension',
|
||||
{
|
||||
cod: () => false,
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
canMakePaymentExtensionsCallbacks[
|
||||
'overwrite-marketplace-extension'
|
||||
].cod
|
||||
).toEqual( firstCodCallback );
|
||||
} );
|
||||
|
||||
it( 'Logs an error if a supplied callback is not a function and does not register the callback for that method', () => {
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'other-woocommerce-marketplace-extension',
|
||||
{
|
||||
cod: false,
|
||||
cheque: () => true,
|
||||
}
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect( console ).toHaveErrored();
|
||||
expect( canMakePaymentExtensionsCallbacks ).toHaveProperty(
|
||||
'other-woocommerce-marketplace-extension'
|
||||
);
|
||||
expect(
|
||||
canMakePaymentExtensionsCallbacks[
|
||||
'other-woocommerce-marketplace-extension'
|
||||
]
|
||||
).not.toHaveProperty( 'cod' );
|
||||
expect(
|
||||
canMakePaymentExtensionsCallbacks[
|
||||
'other-woocommerce-marketplace-extension'
|
||||
]
|
||||
).toHaveProperty( 'cheque' );
|
||||
} );
|
||||
|
||||
it( 'Adds the namespace and callbacks to the canMakePaymentExtensionCallbacks object', () => {
|
||||
// We are using a new namespace here because canMakePaymentExtensionsCallbacks cannot be reset between tests.
|
||||
registerPaymentMethodExtensionCallbacks(
|
||||
'third-woocommerce-marketplace-extension',
|
||||
{
|
||||
cod: () => false,
|
||||
}
|
||||
);
|
||||
expect( canMakePaymentExtensionsCallbacks ).toHaveProperty(
|
||||
'third-woocommerce-marketplace-extension'
|
||||
);
|
||||
} );
|
||||
} );
|
Reference in New Issue
Block a user