Skip to content
System nominalUTC --:--:--Articles 107

The WooCommerce Checkout Fixes That Need No Plugin at All

Six checkout problems that core settings and hooks already fix, ordered by what they cost, plus the threshold where a paid extension starts to earn its licence.

Ana Ferreiro · @aferreiro

WooCommerce engineering

Topic
WOO
Section
WooCommerce & Storefront
Published
17 OCT 2026
Reading time
9 MIN
Words
2183W
Technical cover graphic for The WooCommerce Checkout Fixes That Need No Plugin at All
Generated schematic — WOO series, REV 2.6.

The store sells in three countries, ships from one warehouse and takes cards through one gateway. Its checkout asks for a company name, a phone number and a second address line. None of the three is needed to ship anything, and all three are options in WordPress.

The money goes elsewhere. A store with a checkout problem buys a plugin that bundles a field editor, a one-page layout and cart recovery, then uses the editor to hide the company field. It works, and it brings a stylesheet, extra AJAX and its own checkout template to the page where orders succeed.

Most abandonment on a small store is not persuasion. It is a form asking for something the customer does not have, a checkbox that appears after the payment choice, or a gateway that does nothing when tapped on a phone.

Disclosure: some links here are affiliate links, and WPSifero may earn a commission if you buy through them. That does not change the price you pay or what this piece recommends: the six fixes below are free, and the paid section says when buying is wrong.

Three of the fields you want gone are core options

WooCommerce controls the company field, the phone field and address line 2 with three options: woocommerce_checkout_company_field, woocommerce_checkout_phone_field and woocommerce_checkout_address_2_field. Each takes required, optional or hidden. On the block checkout they sit in the Checkout block's sidebar; on the shortcode checkout they were the Customizer's WooCommerce panel.

hidden is not "required but invisible". The field is dropped from the address field set before the form is built, so validation goes with it. Hiding a required field with CSS leaves an error nobody can resolve.

The rest is one filter. Remove a field only when the order can be fulfilled without it. The email carries the receipt; the postcode sets the shipping rate. A phone is worth keeping if the courier calls before delivery, but not as a required field.

add_filter( 'woocommerce_checkout_fields', function ( $fields ) {
    // Order notes ask for a decision most customers do not want to make.
    unset( $fields['order']['order_comments'] );

    // Only present when the phone option is not set to hidden.
    if ( isset( $fields['billing']['billing_phone'] ) ) {
        $fields['billing']['billing_phone']['required'] = false;
    }

    return $fields;
} );

One limitation. That filter does not run on the block checkout, which takes its fields from the Store API; what works in both is woocommerce_get_country_locale.

Guest checkout is a checkbox, and switching it off costs the order

WooCommerce → Settings → Accounts and privacy has a guest checkout checkbox, and it ships enabled. Stores turn it off because an account makes the customer reachable and the next purchase easier. What it does is put a password between a stranger and a card form.

The failure is blunt. With guest checkout off, registration required and nobody logged in, the checkout template stops before printing the form and outputs one line saying they must be logged in. No form at all, after a tap on a buy button.

The middle setting is the one worth using: guest checkout on, account creation offered, and the password kept out of checkout by the "Send password setup link" option. The customer gets an account; the checkout asks for an email. Where an account is genuinely required, for B2B pricing or licence keys, woocommerce_checkout_registration_required makes the requirement explicit.

The honest cost arrives after the order: no order history, more "where is my order" email, and a repeat rate you cannot see. That is worth paying before the first purchase, and a bad trade for subscriptions.

The terms checkbox prints after the payment methods, the worst place for it

The terms checkbox is not in the checkout form template. It is included from checkout/payment.php, inside the #payment container, below the gateway list and directly above the woocommerce_review_order_before_submit hook that prints the button. Choose how to pay, discover a required checkbox, pay.

That ordering is why the box gets blamed for abandonment it does not cause. The commitment is already made, so a legal box reads as a last demand, and its link opens a wall of terms.

The fix without a plugin is a template override. Copy checkout/payment.php into the theme at woocommerce/checkout/payment.php and move the single wc_get_template( 'checkout/terms.php' ) call to the top of the file, above the gateway loop. The box then lands before the payment choice, on every screen. Overridden templates are yours to maintain: read the version header at the top of the copied file after an upgrade.

The second half is the wording. The default is "I have read and agree to the website [terms]", which points at a page title rather than at what the customer agrees to. It lives in the woocommerce_checkout_terms_and_conditions_checkbox_text option, and the woocommerce_get_terms_and_conditions_checkbox_text filter.

Two limits. With no terms page assigned the box does not print and nothing is enforced, which is why some stores install a plugin to remove a box the page assignment already controls. And do not pre-tick it: woocommerce_terms_is_checked_default exists, and a pre-ticked box is not consent in the EU or the UK.

A payment method that fails on a phone fails without saying anything

The most common mobile payment failure looks like a working page. Card fields are empty or the wallet button does nothing, the button spins, and no error appears. Nothing in the order list says a customer tried and could not, because the request never left the browser.

Four checks, in this order.

  1. Place a test order on a real phone on mobile data, in the gateway's test mode. Device emulation does not reproduce iframe and wallet behaviour.
  2. Watch the network tab for the POST to ?wc-ajax=checkout. If it never fires, the problem is JavaScript, not the gateway.
  3. Turn on the gateway's logging and read WooCommerce → Status → Logs. A gateway that logs nothing on a failed attempt never received the attempt.
  4. Check the optimisation plugin. Checkout has to be excluded from defer, delay and combine, or the payment box stays in its first-render state, telling the customer to fill in their details above.

Two causes are mobile-only. Hosted card fields live in an iframe on the gateway's domain, so a browser blocking third-party content empties the form silently. Wallet buttons are not payment gateways: they never appear in get_available_payment_gateways(), so a country restriction on the gateway does not restrict the wallet, and the wallet fails inside its own sheet.

Make the failure visible on the store side rather than waiting for a customer to report it.

add_action( 'woocommerce_order_status_failed', function ( $order_id ) {
    $order = wc_get_order( $order_id );

    if ( ! $order ) {
        return;
    }

    wc_get_logger()->error(
        sprintf( 'Payment failed on order %s via %s', $order_id, $order->get_payment_method_title() ),
        array( 'source' => 'checkout-watch' )
    );
} );

The country selector is a form-wide reload, not a field

Changing the country selects more than a country. The checkout scripts serialise the whole form, add country, state, postcode, city and address line, and post to ?wc-ajax=update_order_review. The handler writes that address into the session, recalculates totals, and returns two HTML fragments keyed by selector plus a reload flag.

When reload is true the browser reloads the page, and everything typed is gone. Three things set it. A checkout page served from a page cache carries a nonce issued for another session, so the security check fails and the response is a bare {"reload":true}, then the expired-session notice on the second attempt. That is the root cause in the checkout session loss checklist. Second, the chosen payment method stops being available for the new country, which is fixed at the source by limiting selling locations with the woocommerce_allowed_countries option. Third, a custom woocommerce_update_order_review_fragments filter returning form.woocommerce-checkout as a key. Core does that for an expired session; a plugin copying it replaces the form the customer just filled in.

Reading the response

The request returns JSON with a fragments object and a reload boolean. Check reload first: it decides whether the customer keeps what they typed.

The default country is a separate trap. "Default customer location" offers geolocation in two forms, and only one is built for page caching; the plain one writes a guessed country into the page, which the cache serves to everyone. A store that ships to one country should not print a country selector with a single option.

A discount box one click before payment costs more than the coupon is worth

Core keeps the coupon form on the cart page, behind wc_coupons_enabled(), with woocommerce_cart_coupon as the hook. Checkout prints no code input. Discount plugins put one back, usually on woocommerce_review_order_before_submit, above the button.

Either position converts a decision into a search. A visible code box asserts that a code exists, and the customer who believes it opens a second tab. Carts expire. Thirty seconds from an order becomes a tab closed an hour later, and the recovery email has to rescue someone the store sent away. It also selects for the customers most likely to leave, which is how a discount box reads as a conversion win in year one and a margin problem in year two.

The quieter cost is technical. An invalid code fails inside update_checkout, the response carries a notice, and the payment box is replaced mid-entry, which on mobile clears the focused field.

The free decision: coupons enabled, field on the cart page, where the customer is still assembling and a code belongs. If no campaign is running, turn coupons off in Settings → General and the field disappears with it. Paying a plugin to move that field to the payment step is paying for the problem.

Where a paid checkout extension earns its place

Two paid categories deserve an argument, and both sell into a gap core leaves open. Neither fixes anything above, which is why buying one first is usually the mistake.

One page checkout

These collapse cart and checkout into one step by replacing the checkout template and the cart endpoint. They earn their place when the cart page is a detour: a single-product store, or a flow where every decision happens on the product page.

That case has a free answer in core: one filter.

add_filter( 'woocommerce_add_to_cart_redirect', function () {
    return wc_get_checkout_url();
} );

Adding to the cart now lands on the checkout with the item in it. The trade is real: it skips the cart page's quantity editing and its coupon field, so it suits a catalogue sold one item at a time. If that trade is wrong, a one-page plugin is unlikely to be right either.

Where the paid version earns its place is staged data: a delivery window, a gift message, a separate billing entity, a pickup point. Those are a checklist, and a checklist benefits from being split into steps validated one at a time. The checkout extensions on the WooCommerce Marketplace cover that ground, and the free comparison is a well-ordered single page, which core already renders.

Cart abandonment recovery

Recovery tools capture an email before an order exists and send a timed sequence with a link back to the cart, or watch pending and failed orders instead. It has the clearest case of any paid category, and is still bought too early.

It earns its place under three conditions. Abandonment happens at the payment step rather than the field step, so the cart holds a real intention. The store can deliver email, because a sequence in a spam folder recovers nothing. And the average order is large enough that one recovered order a month covers the licence. The cart recovery extensions on the WooCommerce Marketplace are built for that stage; the price is in the low hundreds of dollars a year at the time of writing, so check the current price, they change it.

Below a few hundred orders a month it usually does not earn its place: too few recoverable carts to measure, and the fee is not covered. The free version of the work is the order list filtered to pending and failed, and a month of sending those emails by hand tells you whether anyone comes back.

The sharpest limit is one the tool cannot cross. Recovery assumes the customer forgot. A customer whose card was declined or whose card fields never loaded did not forget, and a reminder to come back and repeat the thing that broke is the wrong message. Those are free to fix.

The rule is a spend order. If the checkout cannot be completed on a phone with a saved card in about a minute, buy nothing and work through the six items here. Then measure one number: how many customers reached the payment step and did not finish. If it is small, the money belongs in traffic. If it is still large after the six fixes, a recovery extension has something to recover, and that is the first licence worth buying. What none of this fixes is a customer who has never heard of the store and does not want to type a card number yet. That is a returns policy and a phone number in the footer, and it costs nothing.

Ana Ferreiro · @aferreiro

WooCommerce engineering

Covers the WooCommerce order lifecycle: checkout, HPOS, session handling, transactional email and the post-purchase layer where most storefront complaints actually originate.

Continue

Related in WooCommerce & Storefront