Classic WooCommerce Checkout Hooks vs Block Alternatives

Migrating a WooCommerce extension from classic checkout to the Checkout Block is not a matter of replacing one PHP hook name with another.

Classic checkout is rendered from PHP templates and extended primarily through do_action() and apply_filters(). The Checkout Block is an interactive frontend application whose extension points include inner blocks, SlotFills, JavaScript filters, data stores, checkout events, the Store API, and a smaller collection of server-side hooks.

Since WooCommerce 8.3, the Cart and Checkout Blocks have been the default for new installations. The classic [woocommerce_checkout] shortcode remains available, however, and many existing stores continue to use it. A production WooCommerce extension therefore often needs to support both checkout architectures. (WooCommerce)

The central migration rule is:

Migrate the purpose of a classic hook, not its position or name.

A classic hook that prints a field, displays a message, validates data, changes a price, or saves order metadata may require a completely different Checkout Block API depending on what the callback actually does.


Classic checkout and Checkout Block are different architectures

ConcernClassic shortcode checkoutCheckout Block
RenderingPHP templatesReact components and WordPress blocks
Main extension mechanismPHP actions and filtersInner blocks, SlotFills, JavaScript filters, Store API extensions, events, and selected PHP hooks
Data sourcePosted form data and WooCommerce sessionWooCommerce data stores and Store API responses
Dynamic updatesWooCommerce AJAX and refreshed fragmentsReactive state and Store API requests
Additional fieldswoocommerce_checkout_fieldsAdditional Checkout Fields API
Arbitrary content placementTemplate-position action hooksInner blocks or predefined SlotFills
Display filteringPHP template filtersCheckout JavaScript filter registry
Custom client dataForm inputs and $_POSTsetExtensionData() and Store API schema
Order-processing hookswoocommerce_checkout_*woocommerce_store_api_checkout_*
Payment UIGateway PHP outputRegistered payment-method React integration

WooCommerce describes Cart and Checkout Block extensibility as a combination of frontend JavaScript interfaces and backend Store API integrations. The Store API provides the customer-facing cart and checkout endpoints used by the block interface. (The WooCommerce Developer Blog)

This architectural difference explains why most classic layout hooks do not fire in the Checkout Block. The PHP templates containing those do_action() calls are not being rendered.


The migration decision table

Before looking for a replacement hook, classify what your existing callback does.

Existing customizationPreferred Checkout Block approach
Add a normal text, select, or checkbox fieldAdditional Checkout Fields API
Add merchant-editable contentCustom inner block
Render extension-owned UI in a predefined locationSlotFill
Change a button label or item labelJavaScript Checkout filter
Send data from a custom React componentsetExtensionData() plus Store API schema
Validate an Additional Checkout Fieldvalidate_callback, validation schema, or additional-field validation hook
Validate a fully custom React interfaceCheckout validation event plus mandatory server validation
Save custom request data to an orderwoocommerce_store_api_checkout_update_order_from_request
Run logic after the Store API creates the orderwoocommerce_store_api_checkout_order_processed
Add or modify a feeExisting woocommerce_cart_calculate_fees hook
Add a payment gatewayPHP gateway plus Checkout Block payment-method registration
Conditionally hide payment methodsPayment-method extension callbacks with server-side enforcement
Modify core address fieldsCheckout editor, field options, or country-locale filters where supported
Refresh output when checkout changesSubscribe to block state or consume Store API data; do not recreate fragment refreshes

There is no universal equivalent of add_action( 'woocommerce_something', ... ) for the Checkout Block.


Hook-by-hook migration map

The following table covers the classic hooks most likely to appear in checkout extensions.

Classic hook or patternCheckout Block alternativeParity
woocommerce_before_checkout_formCustom inner block positioned by the merchantNo exact equivalent
woocommerce_after_checkout_formCustom inner blockNo exact equivalent
woocommerce_checkout_before_customer_detailsInner block within the Checkout Fields areaNo exact equivalent
woocommerce_before_checkout_billing_formAdditional Checkout Field for data; inner block for other UINo exact equivalent
woocommerce_after_checkout_billing_formAdditional Checkout Field or inner blockNo exact equivalent
woocommerce_before_checkout_shipping_formAdditional Checkout Field, shipping inner block, or an applicable SlotFillNo exact equivalent
woocommerce_after_checkout_shipping_formAdditional Checkout Field, shipping inner block, or an applicable SlotFillNo exact equivalent
woocommerce_before_order_notesAdditional Checkout Field in the order location or an inner blockFunctional alternative
woocommerce_after_order_notesAdditional Checkout Field in the order location or an inner blockFunctional alternative
woocommerce_checkout_before_order_reviewInner block in the order-summary areaNo exact equivalent
woocommerce_checkout_after_order_reviewInner block or summary-adjacent SlotFillNo exact equivalent
woocommerce_review_order_before_paymentPayment integration or an inner block near the payment areaNo exact equivalent
woocommerce_review_order_after_paymentPayment integration or inner blockNo exact equivalent
woocommerce_checkout_terms_and_conditionsBuilt-in Terms and Conditions block or custom inner blockEditor-managed
woocommerce_review_order_before_submitInner block, label filter, or payment-specific button integrationNo exact equivalent
woocommerce_review_order_after_submitInner blockNo exact equivalent
woocommerce_checkout_fieldsAdditional Checkout Fields APIPartial replacement
woocommerce_form_field_*No general core-field markup replacementUnsupported
woocommerce_checkout_processField validation API, checkout event, and server validationFunctional alternative
woocommerce_checkout_update_order_metawoocommerce_store_api_checkout_update_order_from_request or woocommerce_store_api_checkout_update_order_metaFunctional alternative
woocommerce_checkout_order_processedwoocommerce_store_api_checkout_order_processedBlock-specific equivalent
woocommerce_checkout_update_order_reviewStore API requests and reactive stateNo direct equivalent
woocommerce_update_order_review_fragmentsNot applicable; blocks do not use classic checkout fragmentsNo equivalent
woocommerce_order_button_textJavaScript placeOrderButtonLabel filterDirect display alternative
woocommerce_order_button_htmlNo global replacement; custom button is available to registered payment methodsLimited
woocommerce_available_payment_gatewaysPartially supported; use block payment registration and payment-method callbacksPartial
woocommerce_cart_item_nameJavaScript itemName filterDirect display alternative
woocommerce_cart_item_remove_linkJavaScript showRemoveItemLink filterDirect display alternative
woocommerce_cart_calculate_feesContinue using the same PHP hookFully supported
wc_add_notice() during checkout processingStore API errors or block validation errorsDifferent behavior

WooCommerce’s official hook audit marks most template-position checkout actions as unsupported because their PHP templates are absent from the block flow. It recommends inner blocks or SlotFills where a suitable insertion point exists. Classic update-order-review actions and fragment filters are also absent because the blocks do not use that AJAX fragment architecture. (The WooCommerce Developer Blog)


1. Replacing woocommerce_checkout_fields

Adding fields is probably the most common classic checkout customization.

Classic checkout implementation

A purchase-order reference can be added to the classic checkout with woocommerce_checkout_fields:

add_filter(
	'woocommerce_checkout_fields',
	static function ( array $fields ): array {
		$fields['order']['hxf_purchase_order_reference'] = array(
			'type'        => 'text',
			'label'       => __(
				'Purchase order reference',
				'hxf-checkout'
			),
			'required'    => false,
			'class'       => array( 'form-row-wide' ),
			'priority'    => 120,
		);

		return $fields;
	}
);

add_action(
	'woocommerce_checkout_create_order',
	static function (
		WC_Order $order,
		array $data
	): void {
		$value = isset(
			$data['hxf_purchase_order_reference']
		)
			? sanitize_text_field(
				(string) $data[
					'hxf_purchase_order_reference'
				]
			)
			: '';

		if ( '' === $value ) {
			return;
		}

		$order->update_meta_data(
			'_hxf_purchase_order_reference',
			$value
		);
	},
	10,
	2
);

This remains a valid implementation for the shortcode checkout, but neither callback provides the equivalent Checkout Block interface.

Checkout Block implementation

Use woocommerce_register_additional_checkout_field:

add_action(
	'woocommerce_init',
	static function (): void {
		if (
			! function_exists(
				'woocommerce_register_additional_checkout_field'
			)
		) {
			return;
		}

		woocommerce_register_additional_checkout_field(
			array(
				'id'            =>
					'hooks-and-filters/purchase-order-reference',
				'label'         => __(
					'Purchase order reference',
					'hxf-checkout'
				),
				'optionalLabel' => __(
					'Purchase order reference (optional)',
					'hxf-checkout'
				),
				'location'      => 'order',
				'type'          => 'text',
				'required'      => false,

				'sanitize_callback' =>
					static function ( $value ): string {
						return sanitize_text_field(
							(string) $value
						);
					},

				'validate_callback' =>
					static function ( $value ) {
						if (
							strlen( (string) $value ) > 80
						) {
							return new WP_Error(
								'hxf_po_reference_too_long',
								__(
									'The purchase order reference must be 80 characters or fewer.',
									'hxf-checkout'
								)
							);
						}
					},
			)
		);
	}
);

The field API currently supports text, select, and checkbox fields. Fields must be registered on woocommerce_init or later, and their IDs must use a namespaced format such as plugin-name/field-name. (The WooCommerce Developer Blog)

The location determines both placement and persistence:

LocationBehavior
contactAppears with contact information and can be associated with the customer
addressAppears in both shipping and billing forms and produces separate values
orderAppears in the Order Information area and is saved to the order

An address field cannot currently be registered for only billing or only shipping through this API. Order fields are appropriate for purchase-order references, gift messages, delivery instructions, referral sources, and similar order-specific data. (The WooCommerce Developer Blog)

Preserving an existing meta key

WooCommerce manages and prefixes metadata created by the Additional Checkout Fields API. Extensions migrating from classic checkout may already have reports, exports, emails, or integrations reading an older key such as _hxf_purchase_order_reference.

You can mirror the block field into that legacy key:

add_action(
	'woocommerce_set_additional_field_value',
	static function (
		$key,
		$value,
		$group,
		$object
	): void {
		if (
			'hooks-and-filters/purchase-order-reference'
				!== $key
			|| ! $object instanceof WC_Order
		) {
			return;
		}

		$object->update_meta_data(
			'_hxf_purchase_order_reference',
			sanitize_text_field( (string) $value )
		);
	},
	10,
	4
);

WooCommerce provides woocommerce_set_additional_field_value for reacting to saves and dynamic woocommerce_get_default_value_for_{field-id} filters for reading values from legacy storage. Its documentation recommends eventually migrating consumers to the managed additional-field values and helper methods. (The WooCommerce Developer Blog)


2. Modifying or removing core checkout fields

A classic extension might unset a field like this:

add_filter(
	'woocommerce_checkout_fields',
	static function ( array $fields ): array {
		unset( $fields['billing']['billing_company'] );

		return $fields;
	}
);

That does not remove the company field from the Checkout Block.

The block intentionally does not use woocommerce_checkout_fields as a general core-field customization API. Company, phone, and Address Line 2 can be configured through the Checkout editor or their corresponding WooCommerce options. Country-specific field visibility and requirements can still be influenced through woocommerce_get_country_locale. (The WooCommerce Developer Blog)

For example:

add_filter(
	'woocommerce_get_country_locale',
	static function ( array $locale ): array {
		if ( isset( $locale['IQ']['postcode'] ) ) {
			$locale['IQ']['postcode']['required'] = false;
		}

		return $locale;
	}
);

Be conservative when removing address data. Payment services can require it for fraud checks, while tax and shipping integrations may require it for calculations. A reduced field set that works with one test gateway can still fail with another gateway in production. WooCommerce explicitly discourages broad removal for this reason. (The WooCommerce Developer Blog)


3. Replacing layout-position hooks

Consider a classic customization like this:

add_action(
	'woocommerce_review_order_before_submit',
	static function (): void {
		echo '<p class="hxf-review-message">';
		echo esc_html__(
			'Orders are reviewed before fulfilment.',
			'hxf-checkout'
		);
		echo '</p>';
	}
);

There is no direct woocommerce_blocks_review_order_before_submit action.

The correct alternative depends on who should control the content.

Use an inner block when the merchant controls placement

A custom inner block is usually the best replacement when the merchant should be able to:

  • Move the content.
  • Remove it.
  • Edit its text or settings.
  • Preview it in the Checkout editor.
  • Place it within a particular checkout section.

Checkout inner-block areas allow a limited set of blocks by default. Extensions can add their own block type to permitted areas with the additionalCartCheckoutInnerBlockTypes JavaScript filter. (The WooCommerce Developer Blog)

const {
	registerCheckoutFilters,
} = window.wc.blocksCheckout;

registerCheckoutFilters(
	'hooks-and-filters',
	{
		additionalCartCheckoutInnerBlockTypes(
			defaultValue,
			extensions,
			args
		) {
			if (
				args?.block ===
				'woocommerce/checkout-shipping-address-block'
			) {
				return [
					...defaultValue,
					'hooks-and-filters/delivery-message',
				];
			}

			return defaultValue;
		},
	}
);

Registering the allowed type does not insert or implement the custom block by itself. The block must still be registered normally, given an appropriate parent, rendered on the frontend, and included in the extension’s build and integration.

Use SlotFill when the extension controls placement

A SlotFill is more appropriate when the extension owns the component and WooCommerce exposes a predefined slot in the required area.

const {
	registerPlugin,
} = window.wp.plugins;

const {
	ExperimentalOrderMeta,
} = window.wc.blocksCheckout;

const CheckoutMessage = () => (
	<div className="hxf-checkout-message">
		Orders are reviewed before fulfilment.
	</div>
);

registerPlugin(
	'hxf-checkout-message',
	{
		render: () => (
			<ExperimentalOrderMeta>
				<CheckoutMessage />
			</ExperimentalOrderMeta>
		),
		scope: 'woocommerce-checkout',
	}
);

SlotFills render external React components in predefined places and provide contextual cart and extension data. ExperimentalOrderMeta, for example, renders beneath the order-summary area. (The WooCommerce Developer Blog)

The Experimental prefix is significant. WooCommerce documents those slots as subject to change or removal before graduating to a stable API. Treat them as version-sensitive integration points and cover them with end-to-end tests. (The WooCommerce Developer Blog)

Do not replace hooks with DOM injection

Avoid code that waits for a selector and inserts arbitrary markup:

// Fragile: do not use this as a block extensibility strategy.
document
	.querySelector( '.wc-block-checkout' )
	?.insertAdjacentHTML(
		'beforeend',
		'<div>Custom content</div>'
	);

The component may rerender, the DOM structure may change, and your inserted node can disappear or be duplicated. DOM position is an implementation detail; an inner block or SlotFill is an extension contract.


4. Replacing woocommerce_order_button_text

The classic implementation is simple:

add_filter(
	'woocommerce_order_button_text',
	static function (): string {
		return __(
			'Complete purchase',
			'hxf-checkout'
		);
	}
);

For the Checkout Block, register the placeOrderButtonLabel JavaScript filter:

const {
	__,
} = window.wp.i18n;

const {
	registerCheckoutFilters,
} = window.wc.blocksCheckout;

registerCheckoutFilters(
	'hooks-and-filters',
	{
		placeOrderButtonLabel(
			defaultValue
		) {
			return __(
				'Complete purchase',
				'hxf-checkout'
			);
		},
	}
);

WooCommerce also provides proceedToCheckoutButtonLabel and proceedToCheckoutButtonLink for the corresponding Cart Block button. These filters return strings; they are not arbitrary button-markup filters. (The WooCommerce Developer Blog)

Replacing the entire Place Order button

There is no general Checkout Block equivalent to:

add_filter(
	'woocommerce_order_button_html',
	'hxf_replace_button_markup'
);

A registered block payment method can provide its own placeOrderButton React component when that payment method requires a specialized payment flow. It can also provide a payment-specific placeOrderButtonLabel. This is part of payment-method registration, not a global checkout customization interface. (The WooCommerce Developer Blog)

Use the standard button unless the payment method itself genuinely requires a custom interaction.


5. Replacing display filters for order-summary items

Classic checkout extensions frequently use PHP filters such as:

woocommerce_cart_item_name
woocommerce_cart_item_class
woocommerce_cart_item_subtotal
woocommerce_cart_item_remove_link

The block frontend does not render those classic PHP templates. WooCommerce instead exposes JavaScript filters for supported portions of cart and order-summary items.

Available filters include:

cartItemClass
cartItemPrice
cartItemScreenReaderPrice
itemName
saleBadgePriceFormat
showRemoveItemLink
subtotalPriceFormat

For example:

const {
	registerCheckoutFilters,
} = window.wc.blocksCheckout;

registerCheckoutFilters(
	'hooks-and-filters',
	{
		itemName(
			defaultValue,
			extensions,
			args
		) {
			const item =
				args?.cartItem;

			if (
				! item ||
				item.type !== 'variation'
			) {
				return defaultValue;
			}

			return `${ defaultValue } — configured`;
		},
	}
);

The filter registry also includes total-label and total-value formatting, button filters, coupon filters, and the inner-block-type filter. Some price-format filters require placeholders such as <price/> to remain in the returned string. (The WooCommerce Developer Blog)

These filters change the block’s presentation. Do not assume that changing a displayed item name also changes the name stored on the order.


6. Passing custom React data to the order

The Additional Checkout Fields API should be the first choice for conventional inputs. A custom inner block is appropriate when you need richer controls, conditional interfaces, third-party widgets, maps, date-selection experiences, or multiple coordinated values.

A custom component can send values through setExtensionData():

import {
	useEffect,
	useState,
} from '@wordpress/element';

export const DeliveryCodeField = ( {
	checkoutExtensionData,
} ) => {
	const [
		deliveryCode,
		setDeliveryCode,
	] = useState( '' );

	const {
		setExtensionData,
	} = checkoutExtensionData;

	useEffect(
		() => {
			setExtensionData(
				'hooks-and-filters',
				'delivery_code',
				deliveryCode
			);
		},
		[
			deliveryCode,
			setExtensionData,
		]
	);

	return (
		<label>
			Delivery code
			<input
				type="text"
				value={ deliveryCode }
				onChange={ ( event ) =>
					setDeliveryCode(
						event.target.value
					)
				}
			/>
		</label>
	);
};

setExtensionData() updates the checkout data store. The namespaced value is subsequently included in the extensions object submitted to the wc/store/checkout endpoint. (The WooCommerce Developer Blog)

Register the expected Store API schema

use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema;

add_action(
	'woocommerce_blocks_loaded',
	static function (): void {
		if (
			! function_exists(
				'woocommerce_store_api_register_endpoint_data'
			)
		) {
			return;
		}

		woocommerce_store_api_register_endpoint_data(
			array(
				'endpoint'  =>
					CheckoutSchema::IDENTIFIER,
				'namespace' =>
					'hooks-and-filters',

				'data_callback' =>
					static function (): array {
						return array();
					},

				'schema_callback' =>
					static function (): array {
						return array(
							'delivery_code' => array(
								'description' => __(
									'Delivery code supplied during checkout.',
									'hxf-checkout'
								),
								'type'        => 'string',
								'context'     =>
									array(
										'view',
										'edit',
									),
								'readonly'    => false,
								'maxLength'   => 40,
								'pattern'     =>
									'^[A-Za-z0-9-]*$',
							),
						);
					},

				'schema_type' => ARRAY_A,
			)
		);
	}
);

The extension namespace prevents your data from colliding with another plugin. The schema defines the values that the endpoint accepts instead of permitting arbitrary unvalidated request properties. WooCommerce’s ExtendSchema implementation wraps the returned property definitions under your registered namespace. (The WooCommerce Developer Blog)

Save the submitted value

add_action(
	'woocommerce_store_api_checkout_update_order_from_request',
	static function (
		WC_Order $order,
		WP_REST_Request $request
	): void {
		$extensions = (array) $request->get_param(
			'extensions'
		);

		$data = isset(
			$extensions['hooks-and-filters']
		)
			? (array) $extensions[
				'hooks-and-filters'
			]
			: array();

		if (
			! isset( $data['delivery_code'] )
		) {
			return;
		}

		$order->update_meta_data(
			'_hxf_delivery_code',
			sanitize_text_field(
				(string) $data['delivery_code']
			)
		);
	},
	10,
	2
);

WooCommerce fires woocommerce_store_api_checkout_update_order_from_request with both the order and full REST request, then persists the order after the action. Calling $order->save() inside this callback is therefore normally unnecessary. (WooCommerce GitHub)

The Store API is a public customer-facing API. Never return private API keys, internal credentials, unrestricted personal information, or other secrets through an extended checkout or cart response. (The WooCommerce Developer Blog)


7. Replacing classic validation hooks

Classic checkout commonly validates with:

add_action(
	'woocommerce_checkout_process',
	'hxf_validate_checkout'
);

or:

add_action(
	'woocommerce_after_checkout_validation',
	'hxf_validate_checkout',
	10,
	2
);

Neither should be treated as the sole validation path for a block integration.

For Additional Checkout Fields

Use the field’s validate_callback, conditional validation schema, or WooCommerce’s additional-field validation hooks. Errors returned by the API are attached to the checkout response and shown by the block interface. (The WooCommerce Developer Blog)

For a custom React component

Use the checkout validation event to catch errors before the checkout request is submitted. The block flow enters its validation stage after the shopper presses Place Order and before the Store API processes the order. WooCommerce exposes checkout statuses and event subscriptions so extension components can participate in that flow. (The WooCommerce Developer Blog)

Client validation improves the experience, but it is not a security boundary. Repeat important eligibility and business-rule checks on the server because a shopper can bypass or manipulate frontend JavaScript.

Avoid relying on wc_add_notice() during Store API requests

Classic checkout extensions often add an error like this:

wc_add_notice(
	__(
		'The supplied reference is invalid.',
		'hxf-checkout'
	),
	'error'
);

WooCommerce documents woocommerce_add_notice as only partially supported for blocks: notices generated during API requests can be stored and not displayed until the next full page load. For immediate checkout feedback, return validation or Store API errors through the API participating in the current request. (The WooCommerce Developer Blog)


8. Replacing order-processing hooks

Classic and block checkout use different order-placement flows.

Classic hookCheckout Block path
woocommerce_checkout_create_orderStore API request/order-update hooks
woocommerce_checkout_update_order_metawoocommerce_store_api_checkout_update_order_meta or woocommerce_store_api_checkout_update_order_from_request
woocommerce_checkout_update_customerwoocommerce_store_api_checkout_update_customer_from_request
woocommerce_checkout_order_processedwoocommerce_store_api_checkout_order_processed

The classic woocommerce_checkout_order_processed action is executed by WC_Checkout during shortcode checkout. It does not fire for orders placed through the Store API. (The WooCommerce Developer Blog)

A block-specific post-processing callback can use:

add_action(
	'woocommerce_store_api_checkout_order_processed',
	static function (
		WC_Order $order
	): void {
		// Perform extension work after the
		// Store API has processed the order.
	}
);

Keep expensive or failure-prone external operations out of the most sensitive checkout path where possible. For example, queueing a synchronization task is usually safer than blocking checkout while an unrelated external CRM responds.

Also make callbacks idempotent. Checkout and payment operations can be retried, and an integration should not create duplicate remote records merely because the same order is observed more than once.


9. Some PHP hooks continue to work unchanged

Do not rewrite every WooCommerce PHP hook simply because the checkout page uses blocks.

Many hooks operate on the cart, products, customers, shipping methods, or totals rather than on classic checkout templates. WooCommerce lists woocommerce_before_calculate_totals, woocommerce_cart_calculate_fees, shipping-rate hooks, stock filters, and numerous product or customer hooks as supported in block flows. (The WooCommerce Developer Blog)

For example, this fee logic works with both checkout implementations:

add_action(
	'woocommerce_cart_calculate_fees',
	static function ( WC_Cart $cart ): void {
		if (
			is_admin() &&
			! wp_doing_ajax()
		) {
			return;
		}

		if ( $cart->is_empty() ) {
			return;
		}

		$cart->add_fee(
			__(
				'Handling fee',
				'hxf-checkout'
			),
			2.50,
			false
		);
	}
);

The distinction is:

  • Business logic hooks operating on WooCommerce objects often continue to work.
  • Template rendering hooks tied to classic HTML positions usually do not.
  • Display filters may require JavaScript equivalents.
  • Classic checkout lifecycle hooks may require Store API equivalents.

Do not confuse fee calculation with fee presentation. woocommerce_cart_calculate_fees works, while classic filters that replace the rendered fee HTML do not necessarily affect the block interface.


10. Payment gateways require a separate block integration

A class extending WC_Payment_Gateway is not, by itself, a complete Checkout Block integration.

The server-side gateway remains responsible for settings, availability, payment processing, order status, refunds, and similar WooCommerce behavior. The block interface additionally requires JavaScript registration through registerPaymentMethod() so it has React components to display inside the payment section. (The WooCommerce Developer Blog)

A payment integration can define:

label
ariaLabel
content
edit
canMakePayment
paymentMethodId
supports
placeOrderButtonLabel
placeOrderButton

For an extension that conditionally hides payment methods it does not own, WooCommerce provides registerPaymentMethodExtensionCallbacks():

const {
	registerPaymentMethodExtensionCallbacks,
} = window.wc.wcBlocksRegistry;

registerPaymentMethodExtensionCallbacks(
	'hooks-and-filters',
	{
		cod: ( args ) => {
			return (
				args.shippingAddress.country
					=== 'IQ'
			);
		},
	}
);

The callback determines frontend availability for the named method. WooCommerce documents one callback per payment method for each extension namespace. (The WooCommerce Developer Blog)

Never depend exclusively on a frontend callback for financial or eligibility rules. Repeat the decision on the server so a modified request cannot use a payment method that should be unavailable.


11. Support classic and block checkout through separate adapters

Do not scatter conditionals such as this throughout the extension:

if ( $is_block_checkout ) {
	// Entire block implementation.
} else {
	// Entire classic implementation.
}

Instead, separate the integration layer from the business rules.

Shared checkout rules
├── Sanitization
├── Validation
├── Eligibility
├── Pricing
└── Order data mapping

Classic checkout adapter
├── PHP field filters
├── Classic validation hooks
└── WC_Checkout order hooks

Checkout Block adapter
├── Additional Fields or custom blocks
├── JavaScript filters and events
├── Store API schema
└── Store API order hooks

For the purchase-order example, both adapters should call the same sanitizer and validator rather than implementing two slightly different rules.

A practical division might be:

final class Purchase_Order_Reference {
	public static function sanitize(
		$value
	): string {
		return sanitize_text_field(
			(string) $value
		);
	}

	public static function is_valid(
		string $value
	): bool {
		return strlen( $value ) <= 80;
	}
}

The classic hook and block field callback can both delegate to this class.

This prevents common dual-support bugs:

  • One checkout accepts a value the other rejects.
  • Classic checkout saves one meta format and block checkout another.
  • One path sanitizes before validation while the other does not.
  • Reports work for classic orders but not block orders.
  • Emails display block data but omit classic data, or vice versa.

12. Declare Checkout Block compatibility only after testing

Once the integration genuinely supports the Cart and Checkout Blocks, declare compatibility:

add_action(
	'before_woocommerce_init',
	static function (): void {
		if (
			! class_exists(
				\Automattic\WooCommerce\Utilities\FeaturesUtil::class
			)
		) {
			return;
		}

		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
			'cart_checkout_blocks',
			__FILE__,
			true
		);
	}
);

When this code is not located in the main plugin file, pass the actual main plugin file path instead of the include file’s __FILE__.

WooCommerce uses the declaration to communicate extension compatibility to merchants. It also expects the plugin header to contain an appropriate WC tested up to value. Do not declare compatibility merely because the extension activates without a fatal error. (The WooCommerce Developer Blog)


13. Compatibility testing matrix

Test both checkout implementations independently.

AreaRequired tests
Checkout pageClassic shortcode and Checkout Block
ShopperGuest and logged-in customer
CartEmpty, simple, variable, virtual, downloadable, and mixed
AddressBilling same as shipping and separate billing address
ShippingMultiple packages, no available rates, local pickup, and free shipping
TaxesInclusive and exclusive configurations where supported
CouponsValid, invalid, removed, and usage-limited
FieldsRequired, optional, invalid, hidden, and prefilled
PaymentsEvery supported standard and express gateway
Order dataHPOS enabled and disabled where the extension supports both
FailuresStore API validation failure, payment failure, retry, and duplicate submission
DevicesDesktop and narrow mobile layouts
AccessibilityKeyboard navigation, labels, focus order, and error announcements
PersistenceOrder admin, emails, exports, REST integrations, and customer account
UpdatesExisting orders and field values created by earlier plugin versions

During block testing, inspect:

Browser console
Store API request payload
Store API response
Checkout data-store state
Order metadata
WooCommerce logs
Network request repetition

A component appearing correctly is not proof that its data is validated, submitted, persisted, exposed to emails, or available to downstream integrations.


Migration checklist

Before declaring a classic checkout extension compatible with the Checkout Block, confirm that:

  • Every classic hook has been classified by purpose.
  • No block feature relies on classic template markup.
  • Conventional fields use the Additional Checkout Fields API.
  • Arbitrary UI uses an inner block or supported SlotFill.
  • Display modifications use documented JavaScript filters.
  • Custom React data uses a namespaced Store API schema.
  • Important validation runs on the server.
  • Classic order hooks have Store API equivalents where required.
  • Payment gateways include frontend block registration.
  • Fees and other shared PHP rules are idempotent.
  • Existing meta keys remain readable during migration.
  • Both checkout implementations use the same business rules.
  • The extension works with HPOS through WooCommerce CRUD objects.
  • Compatibility is declared only after the complete matrix passes.

Conclusion

Classic WooCommerce checkout hooks are built around PHP template positions. Checkout Block extensibility is built around responsibilities and contracts.

That means there is rarely a clean transformation like:

woocommerce_old_hook
→
woocommerce_blocks_new_hook

Instead, the migration usually looks like:

Display a field
→ Additional Checkout Fields API

Insert editable content
→ Inner block

Insert extension-owned component
→ SlotFill

Change a displayed value
→ JavaScript Checkout filter

Send custom client data
→ setExtensionData + Store API schema

Save request data
→ Store API checkout hook

Integrate a gateway
→ PHP gateway + payment-method React registration

Calculate a fee
→ Keep the existing PHP cart hook

The most maintainable extensions treat classic checkout and the Checkout Block as two adapters over one set of business rules.

Do that, and supporting both architectures becomes manageable.

Try to preserve old hook positions through DOM injection, duplicated validation, and undocumented internal components, and every WooCommerce update becomes a compatibility risk.

Official references

Leave a Comment