The plugin is active, the settings are saved, there is no error even in test mode — yet customers never see the payment method at checkout. The cause is usually not the plugin but the type of checkout page.
Since WooCommerce 8.3 new stores get a block-based checkout, which renders payment methods in the browser with React. The block checkout does not automatically list classic gateways that extend WC_Payment_Gateway; each method must also be registered through an AbstractPaymentMethodType class on the woocommerce_blocks_payment_method_type_registration hook, and with registerPaymentMethod in JavaScript. Without that registration there is no error — the method is simply not shown. Because the same plugin works on the shortcode ([woocommerce_checkout]) checkout, the problem is easy to miss.
Under WooCommerce → Settings → Payments the method shows as enabled. Settings are saved, the error log is clean. At checkout, though, only bank transfer or cash on delivery is listed; your custom gateway is not.
We ran into exactly this while reviewing a QNB virtual POS integration for Castor Coffee: in the same multisite install, the retail store used the classic shortcode checkout and the wholesale store used the block checkout. The plugin code was correct and its hash calculations matched what the bank expects — yet on the wholesale site the method would never have appeared, even with the settings completed.
The first question is about the page, not the plugin. Look at the checkout page content:
wp post get $(wp option get woocommerce_checkout_page_id) --field=post_content | head -c 200
<!-- wp:woocommerce/checkout --> — a block checkout[woocommerce_checkout] — a classic checkoutOn multisite, check each site separately; sites set up at different times can have different kinds of checkout page.
The classic checkout is rendered on the server in PHP: WooCommerce walks the available gateways and prints each one's payment_fields(). The block checkout is a React application running in the browser that reads cart and checkout data from the Store API. To know how to display a method it needs a definition registered on the JavaScript side.
WooCommerce does not derive that definition from the classic gateway. The result: your gateway is registered on the server, is_available() returns true, and the block checkout knows nothing about it.
First, a class that introduces the gateway to the block side. The $name must be identical to the gateway's id:
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
final class My_Gateway_Blocks extends AbstractPaymentMethodType {
protected $name = 'my_gateway'; // same as WC_Payment_Gateway::$id
public function initialize() {
$this->settings = get_option( 'woocommerce_my_gateway_settings', array() );
}
public function is_active() {
$gateways = WC()->payment_gateways()->payment_gateways();
return isset( $gateways[ $this->name ] ) && $gateways[ $this->name ]->is_available();
}
public function get_payment_method_script_handles() {
wp_register_script( 'my-gateway-blocks', plugins_url( 'assets/blocks.js', __FILE__ ),
array( 'wc-blocks-registry', 'wc-settings', 'wp-element', 'wp-html-entities' ), '1.0', true );
return array( 'my-gateway-blocks' );
}
public function get_payment_method_data() {
return array( 'title' => 'Credit / Debit Card', 'supports' => array( 'products' ) );
}
}
add_action( 'woocommerce_blocks_payment_method_type_registration', function ( $registry ) {
$registry->register( new My_Gateway_Blocks() );
} );
Reusing the gateway's own is_available() inside is_active() matters: with missing credentials or an unsupported currency the method stays hidden on the block checkout too, and both checkout types follow the same rule.
Also declare block compatibility in the main plugin file, or the editor may flag the plugin as incompatible:
add_action( 'before_woocommerce_init', function () {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
} );
Data returned by get_payment_method_data() is available in the browser under the <name>_data key. The smallest registration, with no build step:
( function () {
var settings = window.wc.wcSettings.getSetting( 'my_gateway_data', {} );
var el = window.wp.element.createElement;
var label = settings.title || 'Credit / Debit Card';
window.wc.wcBlocksRegistry.registerPaymentMethod( {
name: 'my_gateway', // same as $name in PHP
label: el( 'span', null, label ),
ariaLabel: label,
content: el( 'p', null, 'You will be redirected to the secure payment page.' ),
edit: el( 'p', null, 'You will be redirected to the secure payment page.' ),
canMakePayment: function () { return true; },
supports: { features: settings.supports || [ 'products' ] }
} );
} )();
Gateways that redirect the customer to the bank's 3D Secure page need nothing more: when process_payment() returns a redirect URL, the block checkout sends the customer there. Gateways that collect card details on your own page additionally need the onPaymentSetup event to pass payment data to the Store API.
If the method is still missing after registration, check these in order. None of them produces a visible error:
id, the PHP $name and the JavaScript name must all be the samewc.wcBlocksRegistry exists, or not at all — exempt the checkout from optimisationwc-blocks-registry in the dependency list the console shows wc is not definedis_active() returns false: missing settings, an unsupported currency or a mis-built conditionYou can confirm the server-side registration with WP-CLI:
wp eval '
$r = Automattic\WooCommerce\Blocks\Package::container()
->get( Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry::class );
do_action( "woocommerce_blocks_payment_method_type_registration", $r );
echo implode( ", ", array_keys( $r->get_all_registered() ) );'
If your method's name is not in the list, the problem is the PHP registration. If it is listed but still not shown, check in the browser that the script loads and look for console errors; finally, check the is_active() decision.
The block checkout is a separate door for classic payment gateways, and it only opens with a registration. Diagnosis starts with one command: is the checkout a block or a shortcode? If it is a block, write an AbstractPaymentMethodType class and a registerPaymentMethod call for the gateway, keep the name identical in all three places, and do not defer the script. A method that looks ready to take payments but is never shown to customers is one of the most expensive bugs there is — nobody complains, sales simply do not happen.
wp:woocommerce/checkout means block, [woocommerce_checkout] means classicAbstractPaymentMethodType + woocommerce_blocks_payment_method_type_registrationwc.wcBlocksRegistry.registerPaymentMethod()id = PHP $name = JS namewc-blocks-registry, wc-settings, wp-elementdeclare_compatibility( 'cart_checkout_blocks', … )Most often because the checkout page is block-based. Since WooCommerce 8.3 new stores ship with the block checkout, which does not list classic payment gateways automatically. The gateway also has to be registered with an AbstractPaymentMethodType class and with registerPaymentMethod in JavaScript.
Yes — converting the checkout block to the classic shortcode version makes the method appear immediately. It is not a lasting fix, though: WooCommerce ships new features through the block checkout. Making the plugin block-compatible is a one-time job.
Check that the name is identical in all three places: the gateway id, $name in the PHP class and name in JavaScript. Then check whether a speed plugin defers the script and whether is_active() returns true. None of these produces a visible error.
No. When process_payment() returns a redirect URL, the block checkout sends the customer to it. Extra code is only needed for gateways that collect card details on your own page.