
Woocommerce Blocks
- 59 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Customizes WooCommerce checkout and cart Gutenberg blocks using inner blocks, SlotFills, and Store API extensions.
About
Extends the React-powered WooCommerce block cart and checkout with custom inner blocks, SlotFill injections, and Store API data. A developer uses it to add checkout fields or UI without editing core templates.
- Register checkout inner blocks and SlotFill injections
- Extend the Store API to persist custom checkout data
Woocommerce Blocks by the numbers
- 59 all-time installs (skills.sh)
- Ranked #1,218 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill woocommerce-blocksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Customizes WooCommerce checkout and cart Gutenberg blocks using inner blocks, SlotFills, and Store API extensions.
Files
WooCommerce Blocks
Overview
WooCommerce Blocks replaces the classic shortcode-based cart and checkout with React-powered Gutenberg blocks. Custom plugins can extend the Checkout Block by registering inner blocks (custom fields inside checkout steps), using SlotFills (inject UI into predefined injection points), and extending the Store API to save and retrieve custom data. The block-based checkout is the default for new WooCommerce stores since version 8.3.
When to Use This Skill
- When adding custom fields to the checkout form (gift message, delivery date picker, VAT number)
- When injecting promotional content or upsell banners into the cart or checkout block
- When creating a custom checkout step with additional business logic
- When replacing the legacy shortcode checkout on existing WooCommerce sites
- When building a plugin that extends checkout behavior without modifying core templates
Core Instructions
1. Register a Checkout Inner Block
Inner blocks are React components that render inside a checkout step. They require PHP block registration + a JS/React frontend:
<?php
// my-checkout-fields/my-checkout-fields.php
add_action('woocommerce_blocks_loaded', function () {
if (!class_exists('Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface')) {
return;
}
require_once __DIR__ . '/class-my-checkout-integration.php';
add_action(
'woocommerce_blocks_checkout_block_registration',
function ($integration_registry) {
$integration_registry->register(new My_Checkout_Integration());
}
);
}); <?php
// class-my-checkout-integration.php
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
class My_Checkout_Integration implements IntegrationInterface {
public function get_name() {
return 'my-checkout-fields';
}
public function initialize() {
$this->register_block_frontend_scripts();
$this->register_inner_block();
}
private function register_block_frontend_scripts() {
wp_register_script(
'my-checkout-fields-frontend',
plugin_dir_url(__FILE__) . 'build/frontend.js',
['wc-blocks-checkout', 'wp-element'],
filemtime(plugin_dir_path(__FILE__) . 'build/frontend.js'),
true
);
}
private function register_inner_block() {
register_block_type(plugin_dir_path(__FILE__) . 'build/blocks/gift-message/block.json');
}
public function get_script_handles() {
return ['my-checkout-fields-frontend'];
}
public function get_editor_script_handles() {
return [];
}
public function get_script_data() {
return [];
}
}2. Create the inner block React component
// src/blocks/gift-message/index.js
import { registerCheckoutBlock } from "@woocommerce/blocks-checkout";
import { __ } from "@wordpress/i18n";
import { useEffect, useState } from "@wordpress/element";
import {
useExtensionCartUpdateData,
extensionCartUpdate,
} from "@woocommerce/blocks-checkout";
const Block = ({ children, checkoutExtensionData }) => {
const [giftMessage, setGiftMessage] = useState("");
// Persist gift message to cart extension data
const handleChange = (e) => {
const value = e.target.value;
setGiftMessage(value);
extensionCartUpdate({
namespace: "my-checkout-fields",
data: { gift_message: value },
});
};
return (
<div className="wc-block-checkout__gift-message">
<label htmlFor="gift-message">
{__("Gift message (optional)", "my-checkout-fields")}
</label>
<textarea
id="gift-message"
value={giftMessage}
onChange={handleChange}
placeholder={__("Write your message here...", "my-checkout-fields")}
rows={3}
maxLength={200}
/>
<span className="character-count">{giftMessage.length}/200</span>
</div>
);
};
registerCheckoutBlock({
metadata: {
name: "my-checkout-fields/gift-message",
title: "Gift Message",
category: "woocommerce",
parent: ["woocommerce/checkout-shipping-methods-block"],
attributes: {},
},
component: Block,
});3. Extend the Store API to persist custom data
<?php
// Extend the Store API Cart schema to accept and store custom extension data
add_action('woocommerce_blocks_loaded', function () {
woocommerce_store_api_register_endpoint_data([
'endpoint' => Automattic\WooCommerce\StoreApi\Schemas\V1\CartSchema::IDENTIFIER,
'namespace' => 'my-checkout-fields',
'schema_callback' => function () {
return [
'gift_message' => [
'description' => 'Gift message for the order',
'type' => 'string',
'context' => ['view', 'edit'],
'readonly' => false,
'sanitize_callback' => 'sanitize_textarea_field',
],
];
},
'schema_type' => ARRAY_A,
]);
// Save the gift message to WC session when cart is updated
woocommerce_store_api_register_update_callback([
'namespace' => 'my-checkout-fields',
'callback' => function (array $data) {
if (isset($data['gift_message'])) {
WC()->session->set('gift_message', sanitize_textarea_field($data['gift_message']));
}
},
]);
});
// Transfer session data to order meta on checkout
add_action('woocommerce_checkout_order_created', function ($order) {
$gift_message = WC()->session->get('gift_message', '');
if (!empty($gift_message)) {
$order->update_meta_data('_gift_message', $gift_message);
$order->save();
}
});4. Use SlotFills for injecting UI without inner blocks
SlotFills are simpler than inner blocks — they inject content into predefined slots:
// src/frontend.js
import { registerPlugin } from "@wordpress/plugins";
import { ExperimentalOrderMeta } from "@woocommerce/blocks-checkout";
import { __ } from "@wordpress/i18n";
import { useSelect } from "@wordpress/data";
import { CART_STORE_KEY } from "@woocommerce/block-data";
const CartUpsellBanner = () => {
const cartTotal = useSelect((select) => {
const cart = select(CART_STORE_KEY).getCartData();
return cart?.totals?.total_items;
});
const freeShippingThreshold = 5000; // $50.00 in cents
const remaining = freeShippingThreshold - parseInt(cartTotal ?? "0");
if (remaining <= 0) return null;
return (
<div className="free-shipping-banner">
{__(`Add $${(remaining / 100).toFixed(2)} more for free shipping!`, "my-checkout-fields")}
</div>
);
};
registerPlugin("my-cart-upsell", {
render: () => (
<ExperimentalOrderMeta>
<CartUpsellBanner />
</ExperimentalOrderMeta>
),
scope: "woocommerce-checkout",
});5. Build and enqueue assets with `@wordpress/scripts`
// package.json
{
"scripts": {
"build": "wp-scripts build src/frontend.js src/blocks/gift-message/index.js",
"start": "wp-scripts start src/frontend.js src/blocks/gift-message/index.js"
},
"devDependencies": {
"@wordpress/scripts": "^30.0.0"
}
} // src/blocks/gift-message/block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-checkout-fields/gift-message",
"title": "Gift Message",
"category": "woocommerce",
"parent": ["woocommerce/checkout-shipping-methods-block"],
"textdomain": "my-checkout-fields",
"editorScript": "file:../../build/blocks/gift-message/index.js",
"script": "file:../../build/blocks/gift-message/index.js",
"style": "file:../../build/blocks/gift-message/style-index.css"
}Examples
Checkout field validation
Validation of custom checkout data should be done server-side via the Store API update callback. Client-side filters (__experimentalRegisterCheckoutFilters) can only modify display values (e.g., price formatting), not validate form fields.
<?php
// Server-side validation in the Store API update callback
woocommerce_store_api_register_update_callback([
'namespace' => 'my-checkout-fields',
'callback' => function (array $data) {
if (isset($data['gift_message'])) {
$message = sanitize_textarea_field($data['gift_message']);
// Reject messages that contain URLs
if (preg_match('#https?://#i', $message)) {
throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
'invalid_gift_message',
__('Gift messages cannot contain links.', 'my-checkout-fields'),
400
);
}
WC()->session->set('gift_message', $message);
}
},
]);On the client side, handle the error response from extensionCartUpdate to display validation messages:
import { extensionCartUpdate } from "@woocommerce/blocks-checkout";
const handleChange = async (e) => {
const value = e.target.value;
setGiftMessage(value);
try {
await extensionCartUpdate({
namespace: "my-checkout-fields",
data: { gift_message: value },
});
setError(null);
} catch (err) {
setError(err.message);
}
};Disable a payment method for certain cart conditions
<?php
// Hide "Pay Later" payment method if cart contains digital-only products
add_filter(
'woocommerce_blocks_payment_method_type_registration',
function ($payment_method_registry) {
$payment_method_registry->register(
new class implements \Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface {
public function is_active() { return true; }
public function get_payment_method_script_handles() { return []; }
public function get_payment_method_data() { return []; }
public function get_name() { return 'custom-payment-guard'; }
public function initialize() {}
}
);
return $payment_method_registry;
}
);
add_filter('__experimental_woocommerce_blocks_payment_gateway_features_list', function ($features, $name) {
if ($name === 'pay-later') {
// Check if all items in cart are virtual/downloadable
$cart_has_physical = false;
foreach (WC()->cart->get_cart() as $item) {
$product = $item['data'];
if (!$product->is_virtual() && !$product->is_downloadable()) {
$cart_has_physical = true;
break;
}
}
if (!$cart_has_physical) {
$features['available'] = false;
}
}
return $features;
}, 10, 2);Best Practices
- Use `extensionCartUpdate` instead of local state for data that must survive page reload — it stores data in the WooCommerce session via the Store API
- Sanitize all custom data server-side — PHP
sanitize_textarea_field,sanitize_text_field, andabsintare essential for any data written to order meta - Build with `@wordpress/scripts` — it handles dependency extraction, webpack config, and asset versioning automatically for WordPress/Gutenberg projects
- Use `block.json` for inner blocks — the block registry requires a
block.jsonmanifest; it also enables automatic asset loading in the editor - Test both classic and block checkout — some stores may still use the shortcode checkout; conditionally enqueue scripts only when block checkout is detected
- Use the `woocommerce_store_api_register_update_callback` for server validation — client-side validation can be bypassed; always re-validate extension data in the callback
- Prefix all meta keys and namespaces with your plugin slug to avoid conflicts with other plugins
Common Pitfalls
| Problem | Solution |
|---|---|
| Inner block not appearing in editor | Ensure parent in block.json matches the exact block name of the checkout step you're targeting; use browser devtools to confirm the parent block name |
| Extension data not persisting to order | Add woocommerce_checkout_order_created hook to transfer session data to order meta — Store API session data is not automatically copied to orders |
| SlotFill component not rendering | The scope: "woocommerce-checkout" is required in registerPlugin; omitting it or using the wrong scope silently prevents rendering |
Build errors with @wordpress/scripts | The entry points must be specified in package.json scripts or wp-scripts.config.js; by default only src/index.js is built |
| Blocks break on WooCommerce downgrade | Pin @woocommerce/blocks-checkout package version to match the installed WooCommerce version in composer.json |
useSelect(CART_STORE_KEY) returns undefined | Ensure the @woocommerce/block-data package is in the dependencies array of wp_register_script — the store is not globally available |
Related Skills
- @woocommerce-plugin-development
- @woocommerce-rest-api
- @gutenberg-block-development
- @checkout-flow-optimization
- @woocommerce-subscriptions
{
"context": "Tests whether the agent sets up the @wordpress/scripts build system correctly with explicit entry points, creates a valid block.json with required fields, and uses registerCheckoutFilters from @woocommerce/blocks-checkout for client-side checkout validation.",
"type": "weighted_checklist",
"checklist": [
{
"name": "@wordpress/scripts devDependency",
"max_score": 9,
"description": "package.json includes @wordpress/scripts in devDependencies (not dependencies, and not webpack or other custom build tools as the primary build mechanism)"
},
{
"name": "Explicit build entry points",
"max_score": 10,
"description": "package.json build script explicitly lists both entry point files: src/frontend.js and src/blocks/vat-number/index.js (or equivalent paths) as arguments to wp-scripts build"
},
{
"name": "Start script present",
"max_score": 8,
"description": "package.json includes a 'start' (or 'dev') script using wp-scripts start with the same entry points as the build script"
},
{
"name": "block.json apiVersion 3",
"max_score": 8,
"description": "block.json sets apiVersion to 3"
},
{
"name": "block.json parent field",
"max_score": 8,
"description": "block.json includes a parent array containing a woocommerce/* checkout block name (e.g. woocommerce/checkout-contact-information-block or similar)"
},
{
"name": "block.json schema",
"max_score": 7,
"description": "block.json includes the $schema field pointing to https://schemas.wp.org/trunk/block.json"
},
{
"name": "registerCheckoutFilters usage",
"max_score": 12,
"description": "validation.js calls registerCheckoutFilters() imported from @woocommerce/blocks-checkout"
},
{
"name": "validateAdditionalFields filter",
"max_score": 10,
"description": "registerCheckoutFilters registers a validateAdditionalFields handler (or equivalent checkout validation filter)"
},
{
"name": "VAT format validation logic",
"max_score": 10,
"description": "Validation logic checks that the VAT number matches a pattern requiring a 2-letter prefix followed by alphanumeric characters (regex or equivalent check)"
},
{
"name": "Error return structure",
"max_score": 10,
"description": "When validation fails, the filter returns an object with a field key and a message property (not throwing an error or returning a plain string)"
},
{
"name": "@woocommerce/blocks-checkout import in validation",
"max_score": 8,
"description": "validation.js imports registerCheckoutFilters from @woocommerce/blocks-checkout (not from a different package or as a global)"
}
]
}
B2B VAT Number Field: Build Setup and Checkout Validation
Problem/Feature Description
A B2B supplies company sells to businesses across the EU and needs to collect VAT registration numbers at checkout for proper invoicing and VAT exemption processing. A developer has already built the server-side Store API registration and order meta storage for the VAT field. Now they need two things: (1) a proper build configuration so the JavaScript source can be compiled for WordPress, and (2) client-side validation that checks the VAT number format before the customer can complete the order.
The validation requirement is: VAT numbers must start with a 2-letter EU country code followed by 2–13 alphanumeric characters (e.g. "DE123456789", "GB999999973"). If the format is invalid, checkout should be blocked and the customer shown a helpful error message on the VAT field.
The plugin has two JavaScript entry points: src/frontend.js (SlotFill/general frontend) and src/blocks/vat-number/index.js (the inner block). Both must be built.
Output Specification
Produce the following files:
1. package.json — Node.js package file with the build scripts and correct devDependencies for a WordPress/WooCommerce blocks project 2. src/blocks/vat-number/block.json — Block manifest for the VAT number inner block (assume it should appear inside the checkout contact information step) 3. src/blocks/vat-number/validation.js — The client-side validation logic that hooks into WooCommerce checkout filters 4. BUILD_NOTES.md — Brief notes explaining: which build tool is used and why, how to run a development build vs production build, and what the entry points are
The VAT number block already exists — only the build setup and validation file need to be created. The grader will review these four files.
{
"context": "Tests whether the agent correctly implements the WooCommerce Blocks inner block registration pattern, including the PHP IntegrationInterface, Store API extension for data persistence, and proper order meta transfer on checkout. Covers the full stack from PHP bootstrap to React component to server-side save.",
"type": "weighted_checklist",
"checklist": [
{
"name": "woocommerce_blocks_loaded hook",
"max_score": 7,
"description": "Main plugin file uses add_action('woocommerce_blocks_loaded', ...) to register the integration (not init, plugins_loaded, or another hook)"
},
{
"name": "IntegrationInterface class check",
"max_score": 7,
"description": "Bootstrap code checks for existence of Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface before proceeding"
},
{
"name": "IntegrationInterface implementation",
"max_score": 8,
"description": "Integration class implements Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface and defines get_name(), initialize(), get_script_handles(), get_editor_script_handles(), and get_script_data() methods"
},
{
"name": "block.json registration",
"max_score": 7,
"description": "Integration class calls register_block_type() pointing to a block.json file (not registering block attributes manually)"
},
{
"name": "registerCheckoutBlock usage",
"max_score": 8,
"description": "JavaScript file calls registerCheckoutBlock() imported from @woocommerce/blocks-checkout (not registerBlockType from @wordpress/blocks)"
},
{
"name": "@wordpress/element imports",
"max_score": 7,
"description": "React hooks (useState, useEffect, etc.) are imported from @wordpress/element, not from 'react'"
},
{
"name": "extensionCartUpdate for persistence",
"max_score": 9,
"description": "JavaScript component calls extensionCartUpdate() from @woocommerce/blocks-checkout to send data to the server (not storing only in component state or localStorage)"
},
{
"name": "Store API endpoint registration",
"max_score": 8,
"description": "PHP code calls woocommerce_store_api_register_endpoint_data() with endpoint set to CartSchema::IDENTIFIER, a namespace, schema_callback, and schema_type => ARRAY_A"
},
{
"name": "Store API update callback",
"max_score": 8,
"description": "PHP code calls woocommerce_store_api_register_update_callback() with a namespace and callback that saves data to WC()->session"
},
{
"name": "Server-side sanitization",
"max_score": 7,
"description": "PHP callback sanitizes the incoming field value before saving (e.g. sanitize_text_field, sanitize_textarea_field, or similar WordPress sanitization function)"
},
{
"name": "woocommerce_checkout_order_created hook",
"max_score": 9,
"description": "PHP code uses the woocommerce_checkout_order_created action to transfer session data to order meta via $order->update_meta_data() and $order->save()"
},
{
"name": "block.json apiVersion and parent",
"max_score": 7,
"description": "block.json sets apiVersion to 3 and parent array to a woocommerce/* checkout step block name (e.g. woocommerce/checkout-shipping-methods-block)"
},
{
"name": "Prefixed namespace and meta key",
"max_score": 8,
"description": "Store API namespace and order meta key are prefixed with the plugin slug (e.g. 'delivery-date-checkout' or similar), not generic names like 'custom' or 'data'"
}
]
}
Delivery Date Picker for Block Checkout
Problem/Feature Description
A mid-size e-commerce store selling perishable food items has recently migrated to the WooCommerce block-based checkout. Their operations team needs customers to select a preferred delivery date during checkout so that the warehouse can schedule refrigerated shipments accordingly. Without this field, customers call in after placing orders to request specific dates, creating significant manual overhead.
The store's developer needs to create a WordPress plugin that adds a "Preferred Delivery Date" picker field inside the checkout shipping step. When a customer selects a date, it must be reliably recorded on the order so the warehouse management system can read it. The field should appear after the shipping method selection.
Output Specification
Produce the following files for a WordPress plugin called delivery-date-checkout:
1. delivery-date-checkout.php — Main plugin bootstrap file that hooks into WooCommerce Blocks loading 2. class-delivery-date-integration.php — The integration class that registers scripts and the inner block 3. src/blocks/delivery-date/index.js — The React inner block component 4. src/blocks/delivery-date/block.json — Block manifest file
The plugin should:
- Register the delivery date field as an inner block inside the checkout
- Persist the selected date value through the WooCommerce Store API so it survives page reloads
- Save the delivery date to the order on checkout completion
- Sanitize all incoming data before saving
Do NOT actually build/compile the JavaScript — just produce the source files. Include a brief NOTES.md explaining the Store API registration flow.
{
"context": "Tests whether the agent uses the correct WooCommerce SlotFill pattern (registerPlugin with scope and ExperimentalOrderMeta), reads cart data via the correct store and hook, and imports from the right WordPress packages. Covers SlotFill injection, CART_STORE_KEY usage, and scope requirement.",
"type": "weighted_checklist",
"checklist": [
{
"name": "registerPlugin usage",
"max_score": 10,
"description": "frontend.js calls registerPlugin() imported from @wordpress/plugins (not a custom event listener or direct DOM manipulation)"
},
{
"name": "scope: woocommerce-checkout",
"max_score": 12,
"description": "registerPlugin call includes scope: \"woocommerce-checkout\" as a property (the exact string)"
},
{
"name": "ExperimentalOrderMeta slot",
"max_score": 10,
"description": "The plugin render function wraps content in ExperimentalOrderMeta (or another named WooCommerce SlotFill component from @woocommerce/blocks-checkout), not a plain React Fragment or arbitrary div"
},
{
"name": "ExperimentalOrderMeta import source",
"max_score": 8,
"description": "ExperimentalOrderMeta (or the chosen SlotFill component) is imported from @woocommerce/blocks-checkout"
},
{
"name": "useSelect for cart data",
"max_score": 10,
"description": "Cart total is read using useSelect() from @wordpress/data, not via fetch/REST API call or window.wc globals"
},
{
"name": "CART_STORE_KEY usage",
"max_score": 10,
"description": "useSelect passes CART_STORE_KEY imported from @woocommerce/block-data as the store key (not a hard-coded string like 'wc/store/cart')"
},
{
"name": "@woocommerce/block-data import",
"max_score": 8,
"description": "CART_STORE_KEY is imported from @woocommerce/block-data package"
},
{
"name": "@wordpress/element imports",
"max_score": 8,
"description": "Any React hooks or JSX helpers used (e.g. useState, useEffect) are imported from @wordpress/element, not from 'react'"
},
{
"name": "@woocommerce/block-data script dependency",
"max_score": 12,
"description": "plugin-loader.php includes 'wc-blocks-data-store' or '@woocommerce/block-data' equivalent as a script dependency in wp_register_script or wp_enqueue_script"
},
{
"name": "Banner conditional rendering",
"max_score": 12,
"description": "Banner component returns null or nothing when the cart total meets or exceeds the free shipping threshold (does not show when threshold already reached)"
}
]
}
Dynamic Free Shipping Progress Banner for Block Checkout
Problem/Feature Description
An online fashion retailer wants to increase their average order value by showing customers how close they are to earning free shipping during checkout. Their UX team has found that displaying a real-time "Add $X more for free shipping!" message significantly reduces cart abandonment and encourages customers to add one more item before completing their order.
The store runs WooCommerce with the block-based cart and checkout. The development team needs a lightweight JavaScript plugin — no inner block required — that injects a banner into the checkout order summary area. The banner should read the current cart total dynamically and display the remaining amount needed to reach the free shipping threshold of $75.00. Once the threshold is met, the banner should disappear.
The solution must work within the WooCommerce block checkout ecosystem and not require modifying any WooCommerce core templates or PHP files (a pure JavaScript approach is preferred).
Output Specification
Produce the following files:
1. src/frontend.js — The JavaScript/React SlotFill plugin that injects the banner 2. plugin-loader.php — Minimal PHP file that enqueues the compiled frontend script 3. ARCHITECTURE.md — A short document (bullet points are fine) explaining which WooCommerce slot/API was used to inject the banner, why that approach was chosen over an inner block, and how the cart total is read
The banner logic and data-fetching approach are the focus — the grader will review the source files directly, so no build step is needed.
{
"name": "finsi/woocommerce-blocks",
"version": "0.1.0",
"summary": "Gutenberg block-based checkout and cart customization",
"skills": {
"woocommerce-blocks": {
"path": "SKILL.md"
}
}
}