woocommerce/plugins/woocommerce-blocks/packages/checkout/blocks-registry/index.ts

205 lines
5.7 KiB
TypeScript

/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
import { registerBlockComponent } from '@woocommerce/blocks-registry';
import { registerExperimentalBlockType } from '@woocommerce/block-settings';
import type { LazyExoticComponent } from 'react';
import { isObject } from '@woocommerce/types';
/**
* List of block areas where blocks can be registered for use. Keyed by area name.
*/
export type RegisteredBlocks = {
fields: Array< string >;
totals: Array< string >;
contactInformation: Array< string >;
shippingAddress: Array< string >;
billingAddress: Array< string >;
shippingMethods: Array< string >;
paymentMethods: Array< string >;
};
let registeredBlocks: RegisteredBlocks = {
fields: [],
totals: [],
contactInformation: [ 'core/paragraph' ],
shippingAddress: [ 'core/paragraph' ],
billingAddress: [ 'core/paragraph' ],
shippingMethods: [ 'core/paragraph' ],
paymentMethods: [ 'core/paragraph' ],
};
/**
* 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.
*/
const assertType = (
optionName: string,
option: unknown,
expectedType: unknown
): void => {
const actualType = typeof option;
if ( actualType !== expectedType ) {
throw new Error(
`Incorrect value for the ${ optionName } argument when registering a checkout block. It was a ${ actualType }, but must be a ${ expectedType }.`
);
}
};
/**
* Validation to ensure an area exists.
*/
const assertValidArea = ( area: string ): void => {
if ( ! registeredBlocks.hasOwnProperty( area ) ) {
throw new Error(
`Incorrect value for the "area" argument. It was a ${ area }, but must be one of ${ Object.keys(
registeredBlocks
).join( ', ' ) }.`
);
}
};
/**
* Validate the block name.
*
* @throws Will throw an error if the blockname is invalid.
*/
const assertBlockName = ( blockName: string ): void => {
assertType( 'blockName', blockName, 'string' );
if ( ! blockName ) {
throw new Error(
`Value for the blockName argument must not be empty.`
);
}
};
/**
* 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.
*/
const assertOption = (
options: Record< string, unknown >,
optionName: string,
expectedType: 'array' | 'object' | 'string' | 'boolean' | 'number'
): void => {
const actualType = typeof options[ optionName ];
if ( expectedType === 'array' ) {
if ( ! Array.isArray( options[ optionName ] ) ) {
throw new Error(
`Incorrect value for the ${ optionName } argument when registering a checkout block component. It was a ${ actualType }, but must be an array.`
);
}
} else if ( actualType !== expectedType ) {
throw new Error(
`Incorrect value for the ${ optionName } argument when registering a checkout block component. It was a ${ actualType }, but must be a ${ expectedType }.`
);
}
};
/**
* 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.
*/
const assertBlockComponent = (
options: Record< string, unknown >,
optionName: string
) => {
const optionValue = options[ optionName ];
if ( optionValue ) {
if ( typeof optionValue === 'function' ) {
return;
}
if (
isObject( optionValue ) &&
optionValue.$$typeof &&
optionValue.$$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.`
);
};
/**
* Adds a block (block name) to an area, if the area exists. If the area does not exist, an error is thrown.
*/
const registerBlockForArea = (
area: keyof RegisteredBlocks,
blockName: string
): void | Error => {
assertValidArea( area );
registeredBlocks = {
...registeredBlocks,
[ area ]: [ ...registeredBlocks[ area ], blockName ],
};
};
/**
* Get a list of blocks available within a specific area.
*/
export const getRegisteredBlocks = (
area: keyof RegisteredBlocks
): Array< string > => {
assertValidArea( area );
return [ ...registeredBlocks[ area ] ];
};
export type CheckoutBlockOptions = {
// This is a component to render on the frontend in place of this block, when used.
component:
| LazyExoticComponent< React.ComponentType< unknown > >
| ( () => JSX.Element );
// Area(s) to add the block to. This can be a single area (string) or an array of areas.
areas: Array< keyof RegisteredBlocks >;
// Standard block configuration object. If not passed, the block will not be registered with WordPress and must be done manually.
configuration?: BlockConfiguration;
};
/**
* Main API for registering a new checkout block within areas.
*/
export const registerCheckoutBlock = (
blockName: string,
options: CheckoutBlockOptions
): void => {
assertBlockName( blockName );
assertOption( options, 'areas', 'array' );
assertBlockComponent( options, 'component' );
/**
* If provided with a configuration object, this registers the block with WordPress.
*/
if ( options?.configuration ) {
assertOption( options, 'configuration', 'object' );
registerExperimentalBlockType( blockName, {
...options.configuration,
category: 'woocommerce',
parent: [],
} );
}
/**
* This enables the inner block within specific areas. See RegisteredBlocks.
*/
options.areas.forEach( ( area ) =>
registerBlockForArea( area, blockName )
);
/**
* This ensures the frontend component for the checkout block is available.
*/
registerBlockComponent( {
blockName,
component: options.component,
} );
};