A declined card is a polite failure. When the WooCommerce Blocks checkout on a store I was building was tested with the test card 4000 0000 0000 0002, a red "Your card was declined." appeared inline under the card number, plus a banner at the top of the page. A test order that went through was just as easy to read: the page moved straight to order-received without stopping at a Stripe page, and the order became Processing within 3 seconds through the webhook.
The troublesome one is the third ending. Place Order gets pressed, nothing happens, and not a single request goes out. The cause turned out not to be code. An old saved address on the test account had no state, and the validation error existed only in the wc/store/validation data store. One part of my notes about that cause also does not match the WooCommerce code I can read, and I leave that part open below.
No request means the problem is still in the browser
When no request leaves the page, Stripe and the webhook never get a say. The store takes payments through the WooCommerce Stripe Gateway plugin, version 10.9.1, but for this bug the plugin does not matter, because nothing was sent anywhere. Whatever held the button back happened in the browser, before the checkout decided to send anything.
The browser side of a Blocks checkout is a React tree. A comment in the store's mu-plugin for the cart and checkout sums it up in one sentence: both pages are WooCommerce Blocks, "so there is no PHP template to override and no hook inside the React tree". Even the button's label lives in JavaScript. In WooCommerce 11.1.0, the Place order button, the Proceed button and the four checkout step blocks have no title or label property as a block attribute, which was checked one by one through /wp-json/wp/v2/block-types/. Those labels are strings inside WooCommerce's JavaScript, so this store replaces them through wp.i18n.setLocaleData on the wp-i18n handle, only on the cart and checkout pages.
If even the button text lives in JavaScript, the error holding the button back is worth looking for there too. Blocks checkout state is kept in wp.data stores, and the store's own script already reads one of them, the cart store, through window.wp.data.select('wc/store/cart').
The error exists, but only in the validation store
For this silent button, the validation error existed only in the wc/store/validation store. From the outside, the pay button simply seemed dead.
The command for reading that store is not copied from the project notes. I derived it from the wc-blocks-data.js bundle in WooCommerce 10.6.2, which keeps the store key as the string "wc/store/validation", registers it through createReduxStore and then register from window.wp.data, and gives it four selectors: getValidationError, getValidationErrorId, getValidationErrors and hasValidationErrors. Following the same pattern as the cart store above, the minimal form looks like this:
// Minimal example, derived from the WooCommerce 10.6.2 bundle.
// Run it in the browser console on the checkout page.
const v = window.wp.data.select('wc/store/validation');
v.hasValidationErrors(); // true if there is even one error
v.getValidationErrors(); // the whole validation storeThe selectors themselves explain why those two calls are enough. In the same bundle, getValidationErrors points to a function called ta, hasValidationErrors to ea, and getValidationErrorId to Js:
// wc-blocks-data.js, WooCommerce 10.6.2, quoted per selector from the minified code
ta=e=>e
ea=e=>Object.keys(e).length>0
Js=(e,t)=>{if(e.hasOwnProperty(t)&&!e[t].hidden)return`validate-error-${t}`}getValidationErrors returns the whole store state as it is. hasValidationErrors is true as long as that state has at least one key, so a single error is enough. The most interesting one for a case like this is getValidationErrorId. It only returns the id of the error element, validate-error- followed by the error's key, when the error exists in the state and is not flagged hidden. In this code, an error can sit in the store without getting an element id.
There is also a path for bringing hidden errors out. In checkout-frontend.js 10.6.2, one component only acts when three conditions hold together: the checkout is idle, the checkout has an error, and the validation store contains errors. When all three are true, the component calls showAllValidationErrors, and 50 ms later scrolls to the first element matching input:invalid, .has-error input, .has-error select.
Whether that path is the one that failed to fire on the store, I do not know, and I am not going to guess. The code I can quote comes from a WooCommerce 10.6.2 copy installed in another project's folder on my disk, while the store's own code comment names WooCommerce 11.1.0, checked against the live page on 10 September. What was recorded from the store itself comes down to two things: the button stayed silent without a request, and the error was in the validation store.
One empty field in a saved address
The source of the problem was data. The test account used for checkout carried an old saved address with no state, and that address is what made Place Order go silent. My notes explain it like this: WooCommerce requires a state for AE, the country code for the United Arab Emirates, and that state is a text field.
The fix was data as well. The test account's saved address now has the state "Dubai", and no code was changed for it.
The 10.6.2 code only backs half of that note
The word "requires" in that note is something I cannot match against the code I have. In WooCommerce 10.6.2, get_country_locale() actually marks the state for AE as not required:
// includes/class-wc-countries.php, WooCommerce 10.6.2
'AE' => array(
'postcode' => array(
'required' => false,
'hidden' => true,
),
'state' => array(
'required' => false,
),
),That value does not change on its way to the Blocks checkout either. CartCheckoutUtils builds $country_locales with an array_map over get_country_locale(), and the function inside only renames priority to index and drops class. The result fills each country's locale in get_country_data(), and the Checkout block then registers that country data as the countryData setting. The required flag passes through untouched.
The "text field" part makes more sense. There is no state list for the United Arab Emirates in the 10.6.2 i18n/states.php. The only entry keyed 'AE' in that file is 'Armed Forces (AE)', and it sits inside the list of US states. Without a list, there is nothing to offer in a dropdown anyway. If you check the same case yourself, keep in mind that searching that file for the key 'AE' takes you to the Armed Forces, not to the Emirates.
Whether the state really is required in 11.1.0, and if so where that requirement comes from, I have no evidence, because I have not compared the same files in 11.1.0. What does not move is the observation on the store itself.
The test account that carries every test
This bug lived in one account's data, so that account needs a little context. The store was built directly on the production site, starting 30 August 2026, and every cart mutation and test order had to go through a single tester account. A WooCommerce cart sticks to each account on the server, and using the admin account for testing once overwrote the cart I was using.
On 9 September it turned out that the tester account actually had the administrator role, not customer. Because of that it got past coming-soon mode and had the manage_woocommerce capability. At the time the store was still coming-soon to the public, and that mode blocked every non-admin while the private link did not work, so the view of anyone other than the owner could not be tested live.
For this bug the consequence is plain. I cannot say what the silent button looks like to a real buyer, because the live checkout was only reachable by administrator accounts and the test account itself was one. Downgrading it to customer would make tests represent real buyers, but at the time I had not made that decision.
What was applied, and what is not proven
The address change is in place. What the notes do not contain is a retest tied directly to that change: there is no line saying Place Order was tried again and actually sent a request, and the date of the fix is not recorded either.
My notes do hold successful test orders made with the tester account, one on 3 September and one full payment test in the early hours of 10 September that ended as processing through the webhook. Neither is linked to adding the state, and as long as the date of the fix is unknown, I am not using them as proof. So the status is: applied, not verified by any recorded test.
When the pay button seems dead again
The lesson recorded from this case is a single sentence: when the pay button seems dead, check wc/store/validation first. Spelled out a little, the order goes roughly like this.
- First confirm whether any request goes out when the button is pressed. On this store, a failure on the payment side came with its own message, like the declined card above. If there is no request at all, the problem is still in the browser.
- Read the validation store with
hasValidationErrors()andgetValidationErrors(). A single key is enough to make the first one return true. - If the checkout uses a saved address, inspect the saved data, not just how the form looks. In this case, one empty state field in an old address was enough to make the button go silent.
- Check notes about required fields, your own included, against the code of the version that is actually running. My own note was only half backed by the 10.6.2 code.