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
| Concern | Classic shortcode checkout | Checkout Block |
|---|---|---|
| Rendering | PHP templates | React components and WordPress blocks |
| Main extension mechanism | PHP actions and filters | Inner blocks, SlotFills, JavaScript filters, Store API extensions, events, and selected PHP hooks |
| Data source | Posted form data and WooCommerce session | WooCommerce data stores and Store API responses |
| Dynamic updates | WooCommerce AJAX and refreshed fragments | Reactive state and Store API requests |
| Additional fields | woocommerce_checkout_fields | Additional Checkout Fields API |
| Arbitrary content placement | Template-position action hooks | Inner blocks or predefined SlotFills |
| Display filtering | PHP template filters | Checkout JavaScript filter registry |
| Custom client data | Form inputs and $_POST | setExtensionData() and Store API schema |
| Order-processing hooks | woocommerce_checkout_* | woocommerce_store_api_checkout_* |
| Payment UI | Gateway PHP output | Registered 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 customization | Preferred Checkout Block approach |
|---|---|
| Add a normal text, select, or checkbox field | Additional Checkout Fields API |
| Add merchant-editable content | Custom inner block |
| Render extension-owned UI in a predefined location | SlotFill |
| Change a button label or item label | JavaScript Checkout filter |
| Send data from a custom React component | setExtensionData() plus Store API schema |
| Validate an Additional Checkout Field | validate_callback, validation schema, or additional-field validation hook |
| Validate a fully custom React interface | Checkout validation event plus mandatory server validation |
| Save custom request data to an order | woocommerce_store_api_checkout_update_order_from_request |
| Run logic after the Store API creates the order | woocommerce_store_api_checkout_order_processed |
| Add or modify a fee | Existing woocommerce_cart_calculate_fees hook |
| Add a payment gateway | PHP gateway plus Checkout Block payment-method registration |
| Conditionally hide payment methods | Payment-method extension callbacks with server-side enforcement |
| Modify core address fields | Checkout editor, field options, or country-locale filters where supported |
| Refresh output when checkout changes | Subscribe 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 pattern | Checkout Block alternative | Parity |
|---|---|---|
woocommerce_before_checkout_form | Custom inner block positioned by the merchant | No exact equivalent |
woocommerce_after_checkout_form | Custom inner block | No exact equivalent |
woocommerce_checkout_before_customer_details | Inner block within the Checkout Fields area | No exact equivalent |
woocommerce_before_checkout_billing_form | Additional Checkout Field for data; inner block for other UI | No exact equivalent |
woocommerce_after_checkout_billing_form | Additional Checkout Field or inner block | No exact equivalent |
woocommerce_before_checkout_shipping_form | Additional Checkout Field, shipping inner block, or an applicable SlotFill | No exact equivalent |
woocommerce_after_checkout_shipping_form | Additional Checkout Field, shipping inner block, or an applicable SlotFill | No exact equivalent |
woocommerce_before_order_notes | Additional Checkout Field in the order location or an inner block | Functional alternative |
woocommerce_after_order_notes | Additional Checkout Field in the order location or an inner block | Functional alternative |
woocommerce_checkout_before_order_review | Inner block in the order-summary area | No exact equivalent |
woocommerce_checkout_after_order_review | Inner block or summary-adjacent SlotFill | No exact equivalent |
woocommerce_review_order_before_payment | Payment integration or an inner block near the payment area | No exact equivalent |
woocommerce_review_order_after_payment | Payment integration or inner block | No exact equivalent |
woocommerce_checkout_terms_and_conditions | Built-in Terms and Conditions block or custom inner block | Editor-managed |
woocommerce_review_order_before_submit | Inner block, label filter, or payment-specific button integration | No exact equivalent |
woocommerce_review_order_after_submit | Inner block | No exact equivalent |
woocommerce_checkout_fields | Additional Checkout Fields API | Partial replacement |
woocommerce_form_field_* | No general core-field markup replacement | Unsupported |
woocommerce_checkout_process | Field validation API, checkout event, and server validation | Functional alternative |
woocommerce_checkout_update_order_meta | woocommerce_store_api_checkout_update_order_from_request or woocommerce_store_api_checkout_update_order_meta | Functional alternative |
woocommerce_checkout_order_processed | woocommerce_store_api_checkout_order_processed | Block-specific equivalent |
woocommerce_checkout_update_order_review | Store API requests and reactive state | No direct equivalent |
woocommerce_update_order_review_fragments | Not applicable; blocks do not use classic checkout fragments | No equivalent |
woocommerce_order_button_text | JavaScript placeOrderButtonLabel filter | Direct display alternative |
woocommerce_order_button_html | No global replacement; custom button is available to registered payment methods | Limited |
woocommerce_available_payment_gateways | Partially supported; use block payment registration and payment-method callbacks | Partial |
woocommerce_cart_item_name | JavaScript itemName filter | Direct display alternative |
woocommerce_cart_item_remove_link | JavaScript showRemoveItemLink filter | Direct display alternative |
woocommerce_cart_calculate_fees | Continue using the same PHP hook | Fully supported |
wc_add_notice() during checkout processing | Store API errors or block validation errors | Different 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:
| Location | Behavior |
|---|---|
contact | Appears with contact information and can be associated with the customer |
address | Appears in both shipping and billing forms and produces separate values |
order | Appears 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 hook | Checkout Block path |
|---|---|
woocommerce_checkout_create_order | Store API request/order-update hooks |
woocommerce_checkout_update_order_meta | woocommerce_store_api_checkout_update_order_meta or woocommerce_store_api_checkout_update_order_from_request |
woocommerce_checkout_update_customer | woocommerce_store_api_checkout_update_customer_from_request |
woocommerce_checkout_order_processed | woocommerce_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.
| Area | Required tests |
|---|---|
| Checkout page | Classic shortcode and Checkout Block |
| Shopper | Guest and logged-in customer |
| Cart | Empty, simple, variable, virtual, downloadable, and mixed |
| Address | Billing same as shipping and separate billing address |
| Shipping | Multiple packages, no available rates, local pickup, and free shipping |
| Taxes | Inclusive and exclusive configurations where supported |
| Coupons | Valid, invalid, removed, and usage-limited |
| Fields | Required, optional, invalid, hidden, and prefilled |
| Payments | Every supported standard and express gateway |
| Order data | HPOS enabled and disabled where the extension supports both |
| Failures | Store API validation failure, payment failure, retry, and duplicate submission |
| Devices | Desktop and narrow mobile layouts |
| Accessibility | Keyboard navigation, labels, focus order, and error announcements |
| Persistence | Order admin, emails, exports, REST integrations, and customer account |
| Updates | Existing 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
- WooCommerce Hook Alternatives reference. (The WooCommerce Developer Blog)
- Cart and Checkout extensibility guide. (The WooCommerce Developer Blog)
- Additional Checkout Fields API. (The WooCommerce Developer Blog)
- Slot and Fill reference. (The WooCommerce Developer Blog)
- Checkout JavaScript filters. (The WooCommerce Developer Blog)
- Store API custom-field integration. (The WooCommerce Developer Blog)
- Checkout payment-method integration. (The WooCommerce Developer Blog)
- Core checkout-field removal and configuration. (The WooCommerce Developer Blog)