Conditionally Required Checkout Phone Number

Learn how to make the phone number field in WooCommerce checkout required for some shipping methods, and optional for others. Great for when you’ve got courier/tracked shipping methods that require a phone number, but you don’t want to make phone number a mandatory field for all your customers.

Shipping Method

We won’t do this the “proper” way and validate the field requirement in the back-end… Instead, we’ll do it in the front-end using JavaScript/jQuery, so we can dynamically change whether the field is required when the user changes their shipping method. This should give us a good user experience.

importantBefore starting, make sure you’re using a custom child theme so you can edit your theme’s functions.php file.

importantMake sure that Phone Number is set to optional by default. In your website, go to the Customizer > WooCommerce > Checkout and you can set “Phone Field” to Optional in there.

Optional Phone Field in WooCommerce checkout
WooCommerce Checkout : Phone

Break Down the Problem

There’s all sorts going on with the WooCommerce checkout page, with Ajax partial page loads and form validation. There are also multiple WooCommerce JavaScript events being fired at various stages. It’s tempting to jump straight in and look for the shipping method radio buttons, but that section of the page is dynamic. So… if you connect event handlers to the shipping method radio inputs when the page loads, those handlers will disappear when that part of the page is reloaded, and you’d need to connect new handlers, based on the new radio options.

Although it feels like a bit of a hack, the simplest thing to do is scan the selected shipping method text for a known phrase. In this example, we’re going to scan for “(Tracked)”. Note the brackets. If we searched for the phrase “Tracked” then we might get a match on shipping methods called “non-tracked” too, which would be a false positive. So… go through all the shipping methods in your site that require a phone number, and make sure they have the word “(Tracked)” in the description.

We also need to deal with the situation when only a single shipping method is available. When this happens, WooCommerce doesn’t present it as a radio button, and this single shipping method may, or may not, require a phone number.

Our Functions…

  • refreshPhoneNumberRequirement()
    • Update the phone number field to show required or optional.
    • If it’s changed from one state to the other, trigger a WooCommerce form re-validate event.
  • hookShippingMethodChange()
    • Handle when the selected shipping method is changed.

Event Handlers…

  1. When the phone number field changes focus, e.g. when the user presses Enter or Tab:
    • Remove the woocommerce-validated and woocommerce-invalid CSS classes so it’s valid status is effectively “unknown”
    • Assign either woocommerce-validated or woocommerce-invalid, depending on if the phone number is required and/or if it’s been filled-in.
  2. When the page raises the checkout_place_order event, e.g. when the user clicks the “Place Order” button
    • If the phone number is required and is missing, then…
      • show an alert to the user explaining why we need the phone number.
      • Cancel the event to stop the form being POSTed.
  3. When the page raises the updated_checkout event, usually when the customer’s address has changed so the shipping methods are changing…
    1. Connect event handlers to the shipping method radio input fields.
    2. Call our function to refresh the phone number required status.

The PHP Code

We’re going to create two code files; a PHP function to load our JavaScript file, and the JavaScript file that does the actual work. All the site-specific settings will be set at the top of the PHP file, so you can deploy the JavaScript file on multiple projects without having to change it.

The only thing the PHP code needs to do is detect when the checkout page is being loaded, then enqueue our JS file, using the standard wp_enqueue_scripts action and pass our site’s parameters into it.

In your custom child theme, create a new file called “conditional-phone-number.php” and a folder called “conditional-phone-number”. Open conditional-phone-number.php and paste the following into it.

<?php

/**
 * WP Tutorials : Conditionally required phone number (WPT_CRPN)
 *
 * https://wp-tutorials.tech/refine-wordpress/conditionally-required-checkout-phone-number/
 */

defined('WPINC') || die(); // Prevent direct access to this file.

// If any of these phrases are found in the selected shipping method name,
// then the phone number is required.
const WPT_CRPN_PHRASESS_FOR_REQURIED_PHONE = array('(Tracked)', '(Courier)', '(Signed)');

// Pop up alert text when a phone number is required, but not entered.
const WPT_CRPN_MISSING_PHONE_ALERT_TEXT = 'Phone Number is required for tracked deliveries';

const WPT_CRPN_MISSING_PHONE_REQUIRED_LABEL = '(For the Courier)';
const WPT_CRPN_MISSING_PHONE_OPTIONAL_LABEL = '(Optional)';

function custom_wpt_crpn() {
    // Only enqueue our assets if we are actually on the
    // WooCommerce checkout page.
    if (function_exists('is_checkout') && is_checkout()) {
        $base_uri = get_stylesheet_directory_uri();
        $theme_version = wp_get_theme()->get('Version');

        // Enqueue our JavaScript.
        wp_enqueue_script(
            'wpt-crpn-checkout',
            $base_uri . '/conditional-phone-number/crpn-checkout-page.js',
            array('jquery'),
            $theme_version,
            true
        );

        // Pass some data to our JavaScript in a global variable.
        wp_localize_script(
            'wpt-crpn-checkout',
            'crpnCheckoutData', // This is the name of a variable we can use in JS.
            array(
                'matchPhrases' => WPT_CRPN_PHRASESS_FOR_REQURIED_PHONE,
                'requiredPhoneLabel' => WPT_CRPN_MISSING_PHONE_REQUIRED_LABEL,
                'optionalPhoneLabel' => WPT_CRPN_MISSING_PHONE_OPTIONAL_LABEL,
                'missingPhoneAlert' => WPT_CRPN_MISSING_PHONE_ALERT_TEXT,
            )
        );
    }
}
add_action('wp_enqueue_scripts', 'custom_wpt_crpn');

Although the code looks like it takes over 40 lines… it’s only:

  • A function declaration
  • An if statement
  • Four statements
  • A call to add_action() to tell WordPress when to call our function

…there’s not much to it.

Next, open your child theme’s functions.php file and paste the following into it:

// WP Tutorials : Conditionally required phone number (WPT_CRPN).
require_once dirname(__FILE__) . '/conditional-phone-number.php';

The browser will now try to load crpn-checkout-page.js from your child theme, but only on the checkout page. It’ll fail, of course, because we haven’t written crpn-checkout-page.js yet…

The JavaScript Code

Go into the conditional-phone-number folder and create a new file called crpn-checkout-page.js. Then paste the following lump of code into it.

/**
 * WP Tutorials : Conditionally required phone number (WPT_CRPN)
 *
 * https://wp-tutorials.tech/refine-wordpress/conditionally-required-checkout-phone-number/
 */
(function($) {
    'use strict';
    $(window).on('load', function() {
        console.log('WPT Conditionally Required Phone Number');

        // Only proceed if crpnCheckoutData has been set in by calling
        // wp_localize_script()
        if (typeof crpnCheckoutData != 'undefined') {
            crpnCheckoutData.isTracked = false;

            // Convert our array of match phrases to upper case.
            crpnCheckoutData.matchPhrases = crpnCheckoutData.matchPhrases.map(function(phrase) {
                return phrase.toUpperCase()
            });

            $(document.body).on('blur change', '#billing_phone', function() {
                var wrapper = $(this).closest('.form-row');

                // Reset field validation status to unknown.
                wrapper.removeClass('woocommerce-validated');
                wrapper.removeClass('woocommerce-invalid');

                if (!crpnCheckoutData.isTracked) {
                    // Phone number is optional. Don't do anything.
                } else if ($(this).val().length > 0) {
                    // Phone number has something in it, so it's valid.
                    // You can improve this by checking it only contains
                    // numbers and dashes.
                    wrapper.addClass('woocommerce-validated');
                } else {
                    wrapper.addClass('woocommerce-invalid');
                }
            });

            $('form.checkout').on('checkout_place_order', function(event) {
                var isValid = event.result;
                var phoneNumber = $('#billing_phone').val();

                if (!crpnCheckoutData.isTracked) {
                    // Phone number is optional. OK.
                } else if (phoneNumber.length > 0) {
                    // Phone number field has something in it. OK.
                } else {
                    // Phone number is requried but is empty.
                    // Alert the customer.
                    alert(crpnCheckoutData.missingPhoneAlert);
                    isValid = false;
                    // event.stopPropagation();
                    // event.preventDefault()();
                }

                return isValid;
            });

            $(document.body).on('updated_checkout', function(event, data) {
                hookShippingMethodChange();
            });

            refreshPhoneNumberRequirement();
        }

        function hookShippingMethodChange() {
            $('#shipping_method input[type="radio"]').change(function(event) {
                refreshPhoneNumberRequirement();
            });

            refreshPhoneNumberRequirement();
        }

        function refreshPhoneNumberRequirement() {
            var container = $('#shipping_method');
            var selectedRadioOption = $(container).find('input[type=radio]:checked');
            var selectedHiddenOption = $(container).find('input[type=hidden].shipping_method');
            var inputId = null;

            if (selectedRadioOption.length == 1) {
                // Multiple shipping methods available with radio select inputs.
                inputId = $(selectedRadioOption).prop('id');
            } else if (selectedHiddenOption.length == 1) {
                // Only a single shipping method is available.;
                inputId = $(selectedHiddenOption).prop('id');
            } else {
                // No shipping available, so phone number is not required.
            }

            if (inputId) {
                // Look through the test phrases in the crpnCheckoutData.matchPhrases
                // array and see if any of those are included in the selcted
                // shipping method label.
                var label = $(container).find(`label[for="${inputId}"]`);
                var labelText = label.text().toUpperCase();

                crpnCheckoutData.isTracked = false;
                crpnCheckoutData.matchPhrases.forEach(function(testPhrase) {
                    if (labelText.includes(testPhrase)) {
                        crpnCheckoutData.isTracked = true;
                        return; // Break out of the foreach loop.
                    }
                });
            }

            var newIsPhoneRequried = crpnCheckoutData.isTracked;
            var billingPhoneInput = $('#billing_phone');
            var formRow = $(billingPhoneInput).closest('.form-row');
            var billingPhoneLabel = $('#billing_phone_field label[for="billing_phone"]');
            var suffixOptional = billingPhoneLabel.find('span.optional');
            var suffixRequired = billingPhoneLabel.find('span.phone-required');

            var oldIsPhoneRequired = false;
            if (suffixRequired.length > 0) {
                oldIsPhoneRequired = true;
            }

            if (newIsPhoneRequried == oldIsPhoneRequired) {
                // No change
            } else if (newIsPhoneRequried) {
                // Make sure the phone number is required.
                suffixOptional.remove();
                billingPhoneLabel.append(
                    `<span class="phone-required">${crpnCheckoutData.requiredPhoneLabel}<abbr class="required" title="required">*</abbr></span>`
                );
                formRow.addClass('validate-required');
                // billingPhoneInput.attr('placeholder', 'Required for Tracked Delivery');
            } else {
                // Phone number is optional.
                // billingPhoneInput.attr('placeholder', '');
                suffixRequired.remove();
                billingPhoneLabel.append(`<span class="optional">${crpnCheckoutData.optionalPhoneLabel}</span>`);

                formRow.removeClass('woocommerce-invalid');
                formRow.removeClass('woocommerce-validated');
                formRow.removeClass('validate-required');
            }

            // Trigger a revalidation of the checkout fields.
            $('.validate-required input, .validate-required select').trigger('validate');
        }
    });
})(jQuery);

Save that and test it by putting something into your cart that can be shipped by multiple methods.

importantOn the checkout page, make sure you press CrtlShiftR to force-reload all page assets, including the latest versions of all CSS and JS files. The key combination might be different in Safari, but make sure to bypass the browser cache.

That’s it. Have some fun with it, and if you need to make changes… try to keep the JavaScript file generic so it’ll work on any site, and put your site-specific settings in the PHP file.

Happy shopping, with conditional phone number requirements 😎 👍

Leave a Comment

Your email address will not be published. Required fields are marked *