phpjqueryajaxwoocommerceshipping-method

Custom Shipping Method with additional select field issue on WooCommerce Checkout block


I wrote a WooCommerce plugin to add SF Express service points to the Checkout Block page. The dropdown appears correctly, but the selected option is not passed to PHP, so the order notes do not reflect the chosen service point. Below are the relevant snippets of my PHP and JS code.I've tried logging the POST data and it seems the selected option is not being passed to the PHP function correctly. Any insights or suggestions on what might be wrong?

PHP Code:

<?php
if (!defined('ABSPATH')) {
    exit;
}

if (in_array('woocommerce/woocommerce.php', apply_filters('active_plugins', get_option('active_plugins')))) {
    function sf_express_shipping_method_init() {
        if (!class_exists('WC_Shipping_SF_Express')) {
            class WC_Shipping_SF_Express extends WC_Shipping_Method {
                public function __construct() {
                    $this->id = 'sf_express';
                    $this->method_title = __('SF Express Service Points', 'sf_express');
                    $this->method_description = __('Allows customers to pick up parcels from SF Express Service Points.', 'sf_express');
                    $this->init();
                }

                function init() {
                    $this->init_form_fields();
                    $this->init_settings();
                    $this->enabled = $this->get_option('enabled');
                    $this->title = $this->get_option('title');
                    add_action('woocommerce_update_options_shipping_' . $this->id, array($this, 'process_admin_options'));
                }

                function init_form_fields() {
                    $this->form_fields = array(
                        'enabled' => array(
                            'title' => __('Enable/Disable', 'sf_express'),
                            'type' => 'checkbox',
                            'label' => __('Enable SF Express Service Points', 'sf_express'),
                            'default' => 'yes'
                        ),
                        'title' => array(
                            'title' => __('Title', 'sf_express'),
                            'type' => 'text',
                            'description' => __('This controls the title which the user sees during checkout.', 'sf_express'),
                            'default' => __('SF Express Service Points', 'sf_express'),
                            'desc_tip' => true,
                        ),
                        'service_points_csv' => array(
                            'title' => __('Service Points CSV', 'sf_express'),
                            'type' => 'textarea',
                            'description' => __('Paste the contents of your service points CSV file here.', 'sf_express'),
                            'default' => '',
                        ),
                    );
                }

                public function calculate_shipping($package = array()) {
                    $rate = array(
                        'id' => $this->id,
                        'label' => $this->title,
                        'cost' => '0',
                        'calc_tax' => 'per_order'
                    );
                    $this->add_rate($rate);
                }
            }
        }
    }

    add_action('woocommerce_shipping_init', 'sf_express_shipping_method_init');
    add_filter('woocommerce_shipping_methods', function($methods) {
        $methods['sf_express'] = 'WC_Shipping_SF_Express';
        return $methods;
    });

    add_action('wp_enqueue_scripts', function() {
        wp_enqueue_script('sf_express', plugins_url('/sf-express.js', __FILE__), array('jquery'), '1.0', true);
        wp_localize_script('sf_express', 'sf_express_params', array(
            'ajax_url' => admin_url('admin-ajax.php')
        ));
    });

    // AJAX Handler for fetching service points
    add_action('wp_ajax_fetch_sf_express_service_points', 'fetch_sf_express_service_points');
    add_action('wp_ajax_nopriv_fetch_sf_express_service_points', 'fetch_sf_express_service_points');

    function fetch_sf_express_service_points() {
        $options = get_option('woocommerce_sf_express_settings');
        $csv_data = $options['service_points_csv'];
        $lines = explode("\n", $csv_data);
        $html = '';
        foreach ($lines as $line) {
            $parts = explode(',', trim($line));
            if (count($parts) >= 2) {
                $html .= '<option value="' . esc_attr(trim($parts[0])) . '">' . esc_html(trim($parts[1])) . '</option>';
            }
        }
        echo $html;
        wp_die();
    }

    add_action('woocommerce_checkout_create_order', function($order, $data) {
        // Log the posted service point value
        error_log('Selected SF Express Service Point: ' . print_r($_POST['sf_express_service_point'], true));

        if (!empty($_POST['sf_express_service_point'])) {
            $service_point = sanitize_text_field($_POST['sf_express_service_point']);
            $order->update_meta_data('sf_express_service_point', $service_point);
            $note = 'SF Express Service Point: ' . $service_point;
            $order->add_order_note($note);
        }
    }, 10, 2);

    function debugging( $order_id ) {
        $order = wc_get_order( $order_id );

        // Log the POST data for debugging
        error_log('POST Data: ' . print_r($_POST, true));

        if (isset($_POST['sf_express_service_point']) && !empty($_POST['sf_express_service_point'])) {
            $service_point = sanitize_text_field($_POST['sf_express_service_point']);
            error_log('Service Point: ' . $service_point); // Debug logging
            $order->update_meta_data('sf_express_service_point', $service_point);
            $note = 'Testing: ' . $service_point;
            $order->add_order_note($note);
            $order->save();
        } else {
            $note = 'Testing: Service point not set';
            $order->add_order_note($note);
            $order->save();
        }
    }
    add_action('woocommerce_thankyou', 'debugging', 10, 1);


    add_action('woocommerce_checkout_process', function() {
        if (empty($_POST['sf_express_service_point']) && isset($_POST['shipping_method'][0]) && $_POST['shipping_method'][0] === 'sf_express') {
            wc_add_notice(__('Please select an SF Express Service Point.', 'sf_express'), 'error');
        }
    });

    add_action('woocommerce_admin_order_data_after_billing_address', function($order) {
        $service_point = $order->get_meta('sf_express_service_point');
        if ($service_point) {
            echo '<p><strong>' . __('SF Express Service Point', 'sf_express') . ':</strong> ' . esc_html($service_point) . '</p>';
        }
    });

    add_filter('woocommerce_email_order_meta_fields', function($fields, $sent_to_admin, $order) {
        $service_point = $order->get_meta('sf_express_service_point');
        if ($service_point) {
            $fields['sf_express_service_point'] = array(
                'label' => __('SF Express Service Point', 'sf_express'),
                'value' => esc_html($service_point),
            );
        }
        return $fields;
    }, 10, 3);

    add_action('woocommerce_order_details_after_order_table', function($order) {
        $service_point = $order->get_meta('sf_express_service_point');
        if ($service_point) {
            echo '<p><strong>' . __('SF Express Service Point', 'sf_express') . ':</strong> ' . esc_html($service_point) . '</p>';
        }
    });
}


JS Code:

jQuery(document).ready(function($) {
    console.log('SF Express script loaded');

    function addServicePointField() {
        console.log('Checking if service point field needs to be added');
        if ($('#sf_express_service_point').length === 0) {
            console.log('Adding service point field');
            var servicePointFieldHtml = '<div class="form-row form-row-wide">' +
                '<label for="sf_express_service_point">' +
                'SF Express Service Point' +
                '</label>' +
                '<select id="sf_express_service_point" name="sf_express_service_point" class="select">' +
                '<option value="">Select a service point</option>' +
                '</select>' +
                '</div>';

            // Append to the shipping method container
            $('.wc-block-components-shipping-rates-control__package').append(servicePointFieldHtml);
            updateServicePointField(); // Fetch and update the dropdown
        }
    }

    function removeServicePointField() {
        console.log('Removing service point field if it exists');
        $('#sf_express_service_point').parent().remove();
    }

    function updateServicePointField() {
        console.log('Fetching updated service points');
        $.ajax({
            url: sf_express_params.ajax_url,
            type: 'POST',
            data: {
                action: 'fetch_sf_express_service_points'
            },
            success: function(response) {
                console.log('Service points fetched successfully');
                $('#sf_express_service_point').html(response);
            }
        });
    }

    // Initial call with a delay to ensure all dynamic elements are rendered
    setTimeout(addServicePointField, 1000);

    // Re-add the service point field when the shipping method changes
    $(document).on('change', 'input[name="shipping_method[0]"]', function() {
        var shippingMethod = $(this).val();
        if (shippingMethod === 'sf_express') {
            addServicePointField();
        } else {
            removeServicePointField();
        }
    });

    // Ensure the field is included in the form submission
    $('form.checkout').on('checkout_place_order', function() {
        var servicePoint = $('#sf_express_service_point').val();
        console.log('Selected service point: ' + servicePoint); // Debugging
        if (servicePoint) {
            $('<input>').attr({
                type: 'hidden',
                name: 'sf_express_service_point',
                value: servicePoint
            }).appendTo('form.checkout');
        } else {
            console.log('No service point selected');
        }
    });

    // Trigger the addServicePointField function when the page loads and shipping method is already selected
    if ($('input[name="shipping_method[0]"]:checked').val() === 'sf_express') {
        addServicePointField();
    }
});


The service point selector is displayed, but the selected option is not being saved with the order. I've tried logging the POST data and it seems the selected option is not being passed to the PHP function correctly. Any insights or suggestions on what might be wrong?


Solution

  • First, as you are using WooCommerce Checkout Block, note that it allows very few customizations.

    You will not be able to get the selected "Service Point" to save it as order metadata, and either you will not be able to validate the field if no value has been selected.

    Also, there are multiple mistakes in your code.


    Solution: Using WC_Session variables.

    What you can do, instead, is to save the selected "Service point" in a session variable.

    As you can't validate this custom field, the only way is to remove the empty option, this way you always have a value.

    Also, instead of displaying the options via Ajax, you can display the complete select field with all options from start as hidden. Then, we show or hide the field, depending on the selected shipping method.

    If customer chose "SF Express" as shipping method, we save the selected "Service Point" via Ajax in a WC_Session variable.

    Then when submitting the data, if "SF Express" is the chosen shipping method, we can get the selected "Service Point" from the WC_Session variable.

    Note that the following hooks doesn't seems to work with Checkout blocks:

    But woocommerce_checkout_create_order_shipping_item hook works, so that's why we use it, to save the selected "Service point".

    Your class WC_Shipping_SF_Express method code stays unchanged. We change everything else.

    Note that the following code is only to be used with WooCommerce Checkout Block:

    // Helper function: Get service points in a clean formatted array
    function get_formatted_service_points_array() {
        $sf_express_settings = get_option('woocommerce_sf_express_settings');
        $service_points_csv  = isset($sf_express_settings['service_points_csv']) ? $sf_express_settings['service_points_csv'] : null;
        
        if ( ! $service_points_csv ) {
            return false;
        }
    
        $service_points = (array) explode("\n", $service_points_csv);
        
        if ( ! $service_points ) {
            return false;
        }
        $service_point_array = array(); // Initializing
    
        foreach( $service_points as $service_point ) {
            $option = (array) explode(',', trim($service_point));
    
            if ( isset($option[0], $option[1]) && !empty($option[0]) && !empty($option[1])  ) {
                $service_point_array[esc_attr($option[0])] = esc_html($option[1]);
            }
        }
        return $service_point_array;
    }
    
    // JQuery: Display Service Point field, show/hide the field and Send Ajax request
    add_action('woocommerce_checkout_init', 'sf_express_checkout_init_js');
    function sf_express_checkout_init_js() {
        $service_points = get_formatted_service_points_array();
    
        if ( ! $service_points ) return; // exit
    
        $chosen_point = WC()->session->get('chosen_service_point');
    
        $field_html = '<div class="form-row form-row-wide" id="sf_express_service_point_field" style="margin-top:12px; display:none">' .
            '<label for="sf_express_service_point">' . __('Select a Service Point') . '</label>
            <select id="sf_express_service_point" name="sf_express_service_point" class="wc-select">';
    
        foreach( $service_points as $option => $label ) {
            $selected    = $option === $chosen_point ? ' selected' : '';
            $field_html .= sprintf('<option value="%s" %s>%s</option>', $option, $selected, $label);
        }
    
        $field_html .= '</select></div>'; 
    
        wc_enqueue_js( "var chosenShipping;
        const shippingOpSel = '.wc-block-checkout__shipping-option input';
        
        function saveChosenServicePointViaAjax( value ) {
            const blockWhite = {message: null, overlayCSS: {background: '#fff', opacity: 0.6}};
                formSel = 'form.wc-block-checkout__form';
    
            $(formSel).block(blockWhite);
    
            $.ajax({
                url: '" . admin_url('/admin-ajax.php') . "',
                type: 'POST',
                data: {
                    'action': 'chosen_sf_express_service_point',
                    'service_point': value
                },
                success: function(response) {
                    $(formSel).unblock();
                    console.log('Chosen Service Point saved: '+response);  // To be removed (for testing)
                }
            });
        }
    
        // On start (lightly delayed):
        setTimeout(function(){
            // Append the service points select field (hidden)
            $('.wc-block-components-shipping-rates-control__package').append('{$field_html}');
            chosenShipping = $(shippingOpSel+':checked').val();
    
            // show service points if the chosen shipping method is 'sf_express'
            if( $(shippingOpSel+':checked').val() === 'sf_express' ) {
                $('#sf_express_service_point_field').show();
                saveChosenServicePointViaAjax( $('#sf_express_service_point').val() );
            }
            console.log('Chosen shipping (start): '+$(shippingOpSel+':checked').val()); // To be removed (for testing)
        }, 100);
    
        // On change: show or hide service points based on the chosen shipping method
        $(document.body).on('change', '.wc-block-checkout__shipping-option input', function() {
            chosenShipping = $(this).val();
    
            if (chosenShipping === 'sf_express') {
                $('#sf_express_service_point_field').show();
                saveChosenServicePointViaAjax( $('#sf_express_service_point').val() );
            } else {
                $('#sf_express_service_point_field').hide();
            }
            console.log('Chosen shipping (change): '+chosenShipping); // To be removed (for testing)
        });
    
        // On change: When choosing a service point 
        $(document.body).on('change', '#sf_express_service_point', function() {
            console.log('Chosen point (change): '+$(this).val()); // To be removed (for testing)
            saveChosenServicePointViaAjax( $(this).val() );
        });");
    }
    
    // AJAX Handler for fetching service points
    add_action('wp_ajax_chosen_sf_express_service_point', 'save_chosen_service_point_in_session');
    add_action('wp_ajax_nopriv_chosen_sf_express_service_point', 'save_chosen_service_point_in_session');
    
    function save_chosen_service_point_in_session() {
        if ( isset($_POST['service_point']) ) {
            $service_point = esc_attr($_POST['service_point']);
            WC()->session->set('chosen_service_point', $service_point); // Set value in a session variable
        }
        wp_die(isset($service_point) ? $service_point : 'Error: no service point.');
    }
    
    // Save the chosen service point as order "shipping" item metadata (displayed on admin order shipping item)
    add_action('woocommerce_checkout_create_order_shipping_item', 'save_chosen_service_point_as_order_shipping_meta', 10, 4);
    function save_chosen_service_point_as_order_shipping_meta(  $item, $package_key, $package, $order ) {
        if ( 'sf_express' !== WC()->session->get( 'chosen_shipping_methods' )[$package_key] ) {
            return;
        }
    
        if ( $chosen_point = WC()->session->get('chosen_service_point') ) {
            $item->update_meta_data('Service point', $chosen_point );
        }
    }
    
    // Save the chosen service point as order meta
    add_action( 'woocommerce_order_status_changed', 'save_chosen_service_point_as_order_meta', 10, 4 );
    function save_chosen_service_point_as_order_meta( $order_id, $status_from, $status_to, $order ) {
        // If "Service point order metadata exists we exit
        if ( $order->get_meta('sfe_service_point') ) return; // Exit
    
        foreach ($order->get_items('shipping') as $item ) {
            if ( $item->get_method_id() !== 'sf_express' ) {
                continue;
            }
    
            if ( $service_point = $item->get_meta('Service point') ) {
                $all_points = get_formatted_service_points_array();
                $order->update_meta_data('sfe_service_point', $all_points[$service_point] );
                $order->add_order_note('SF Express Service Point: ' . $all_points[$service_point]);
                $order->save();
                break;
            }
        }
    }
    
    // Remove the session variable if exists
    add_action( 'woocommerce_thankyou', 'remove_sfe_service_point_session_variable', 10, 1 );
    function remove_sfe_service_point_session_variable( $order_id ) {
        if ( WC()->session->__isset('sfe_service_point') ) {
            WC()->session->__unset('sfe_service_point');
        }
    }
    
    // Display on admin orders after shipping address
    add_action('woocommerce_admin_order_data_after_shipping_address', 'admin_order_sfe_service_point_display' );
    function admin_order_sfe_service_point_display($order) {
        if ( $service_point = $order->get_meta('sfe_service_point') ) {
            printf( '<div><h3>%s:</h3> <span>%s</span></div>', 
                __('SF Express Service Point', 'sf_express'),
                esc_html($service_point)
            );
        }
    }
    
    // Display on customer orders (thankyou, order pay, view order)
    add_action( 'woocommerce_order_details_after_order_table', 'customer_order_sfe_service_point_display' );
    function customer_order_sfe_service_point_display( $order ) {
        if ( $service_point = $order->get_meta('sfe_service_point') ) {
            printf('<h2 class="woocommerce-order-service_point__title">%s</h2>
                <table class="woocommerce-table"><tbody><tr><th>%s</th></tr><tbody></table>',
            esc_html__( 'SF Express Service Point', 'woocommerce' ), $service_point );
        }
    }
    
    // Display custom fields data on email notifications
    add_action( 'woocommerce_email_order_details', 'email_notifications_sfe_service_point_display', 20, 4 );
    function email_notifications_sfe_service_point_display( $order, $sent_to_admin, $plain_text, $email ) {
        if ( $service_point = $order->get_meta('sfe_service_point') ) {
    
            echo '<style>
            .service-point table{width: 100%; font-family: \'Helvetica Neue\', Helvetica, Roboto, Arial, sans-serif;
                color: #737373; border: 1px solid #e4e4e4; margin-bottom:8px;}
            .service-point table th, .message table td{text-align: left; border-top-width: 4px;
                color: #737373; border: 1px solid #e4e4e4; padding: 12px; width:58%;}
            .service-point table td{text-align: left; border-top-width: 4px; color: #737373; border: 1px solid #e4e4e4; padding: 12px;}
            </style>';
    
            printf( '<div class="service-point"><h2>%s</h2>
            <table cellspacing="0" cellpadding="6"><tbody>
            <tr><td>%s</td></tr>
            </tbody></table></div><br>',  
            esc_html__( 'SF Express Service Point', 'woocommerce' ), $service_point );
        }
    }
    

    Code goes in functions.php file of your child theme (or in a plugin). Tested and works.


    On customer orders:

    enter image description here


    On email notifications:

    enter image description here


    On admin Order:

    enter image description here