Set a dynamic fee based on custom checkout radio buttons and text field in WooCommerce

I am trying to create a custom checkout radio button that calculates restaurant tip in percentage.

For the radio button, the static value is working fine.

However, I want to get the subtotal and calculate certain percentage on custom radio button click.

This is my code

add_action( 'woocommerce_after_checkout_billing_form', 'add_box_option_to_checkout' );
function add_box_option_to_checkout( $checkout ) {

$chosen = WC()->session->get( 'tip' );
    $chosen = empty( $chosen ) ? WC()->checkout->get_value( 'tip' ) : $chosen;
    $chosen = empty( $chosen ) ? '0' : $chosen;
    
    $total = WC()->cart->get_subtotal();
$fivetip = $total * 0.05;

    woocommerce_form_field( 'tip',  array(
        'type'      => 'radio',
        'class'     => array( 'form-row-wide', 'update_totals_on_change' ),
        'options'   => array(
            $fivetip  => '5%',
            '10.00'     => '10%',
            '15.00'     => '15%',
        ),
    ), $chosen );
    
        woocommerce_form_field( 'add_tip', array(
        'type'          => 'text',
        'class'         => array('add_tipform-row-wide'),
        'placeholder'   => __('Enter Custom Tip Amount') 
        ), $checkout->get_value( 'add_tip' ));
    
}


add_action( 'woocommerce_cart_calculate_fees', 'checkout_tip_fee', 20, 1 );
function checkout_tip_fee( $cart ) {
    if ( $radio = WC()->session->get( 'tip' ) ) {
        $cart->add_fee( 'Tip', $radio );
    }
}

add_action( 'woocommerce_checkout_update_order_review', 'checkout_tip_choice_to_session' );

function checkout_tip_choice_to_session( $posted_data ) {
    parse_str( $posted_data, $output );
    if ( isset( $output['tip'] ) ){
        WC()->session->set( 'tip', $output['tip'] );
    }
}

Answer

The following is something advanced using Ajax and WC Sessions:

It will add a custom a tip (as a custom fee) based on selected radio buttons options: fixed percentages options or custom option that will show a text field to allow customer to input a fixed amount.

Displayed Fields are hand coded to get a better display than WooCommerce form fields for radio buttons (see the screenshots below).

enter image description here

How it works for the customer:

On checkout page load, a Tip of 5% (a fee) is applied (selected by default). When changing the selected option to something else, the applied fee changes.

If the “custom” option is selected, the fee is removed while a text field appears below:

enter image description here
Customer can input a fixed amount and a Tip (a fee) is applied with this amount.

Here is the code:

// Display custom checkout fields
add_action( 'woocommerce_after_checkout_billing_form', 'add_box_option_to_checkout' );
function add_box_option_to_checkout( ) {
    // Here set your radio button options (Values / Labels pairs)
    $options = array( '5'  => '5%', '10' => '10%', '15' => '15%', 'custom' => __('Custom', 'woocommerce') );

    // Radio button fields
    echo '<style> #add_tip_field.form-row label { display:inline-block; margin-left:6px; } </style>
    <p class="form-row form-row-wide" id="add_tip_field"><span class="woocommerce-input-wrapper">
    <label for="add_tip"><strong>'.__('Add a tip', 'woocommerce'). ':</strong>&nbsp;</label>';

    foreach ( $options as $value => $label_name ) {
        $checked = $value == '5' ? ' checked="checked"' : '';

        echo '<label for="add_tip_'.$value.'" class="radio ">
            <input type="radio" class="input-radio " value="'.$value.'" name="add_tip" id="add_tip_'.$value.'"'.$checked.'>&nbsp'.$label_name.'
        </label>';
    }
    echo '</span></p>';

    // Text field (hidden by default)
    echo '<p class="form-row form-row-wide" id="custom_tip_field" style="display:none""><span class="woocommerce-input-wrapper">
        <input type="text" class="input-text " name="custom_tip" id="custom_tip" value="" placeholder="'.__('Input a tip amount', 'woocommerce').'">
    </span></p>';
}

// jQuery / Ajax script
add_action( 'woocommerce_after_checkout_form', 'wc_checkout_fee_script' );
function wc_checkout_fee_script() {
    ?>
    <script type="text/javascript">
    jQuery( function($){
        if (typeof wc_checkout_params === 'undefined')
            return false;

        var addTip    = 'input[name="add_tip"]',
            customTip = 'input[name="custom_tip"]'

        function triggerAjaxEvent( amount, type = 'percent' ){
           $.ajax({
                type: 'POST',
                url: wc_checkout_params.ajax_url,
                data: {
                    'action': 'tip_fee',
                    'amount': amount,
                    'type'  : type
                },
                success: function (result) {
                    $(document.body).trigger('update_checkout');
                    console.log(result);
                },
            });
        }

        triggerAjaxEvent( $(addTip+':checked').val() );

        $('form.checkout').on('change', addTip, function() {
            var textField = $('#custom_tip_field'),
                percent   = $(this).val();

            if( percent === 'custom' && textField.css('display') === 'none' ) {
                textField.show(200);
            } else if ( percent !== 'custom' && textField.css('display') !== 'none' ) {
                textField.hide(200, function(){
                    $(customTip).val('');
                });
            }
            triggerAjaxEvent( percent );
        });

        $('form.checkout').on('input change', customTip, function() {
            triggerAjaxEvent( $(this).val(), 'custom' );
        });
    });
    </script>
    <?php
}

// Get Ajax request and save data to WC session
add_action( 'wp_ajax_tip_fee', 'get_tip_fee' );
add_action( 'wp_ajax_nopriv_tip_fee', 'get_tip_fee' );
function get_tip_fee() {
    if ( isset($_POST['amount']) && isset($_POST['type']) ) {
        $fee  = is_numeric($_POST['amount']) && $_POST['amount'] > 0 ? floatval($_POST['amount']) : 0;

        WC()->session->set('fee_data', array(
            'type'   => esc_attr($_POST['type']),
            'amount' => $fee
        ) );

        print_r(WC()->session->get('fee_data'));
    }
    die();
}


// Add a dynamic fee from WC Session ajax data
add_action( 'woocommerce_cart_calculate_fees', 'checkout_custom_tip_fee' );
function checkout_custom_tip_fee( $cart ) {
    if ( is_admin() && !defined('DOING_AJAX') )
        return;

    $data = WC()->session->get('fee_data');
    $fee  = $total = 0;

    if ( isset($data['type']) && isset($data['amount']) ) {
        // 1. Fixed Fee amount
        if ( $data['type'] === 'custom' ) {
            $text  = $data['type'];
            $fee   = $data['amount'];
        } 
        // 2. Calculated percentage Fee amount
        elseif ( $data['type'] === 'percent' && $data['amount'] > 0 ) {
            $text  = $data['amount'] . '%';

            // Get cart subtotal excl. Taxes (discounted)
            foreach ( $cart->get_cart() as $cart_item ) {
                $total = $cart_item['line_total'];
            }
            // Calculate fee
            $fee   = $total * $data['amount'] / 100 ;
        }
        // Add the fee
        if ( $fee > 0 ) {
            $cart->add_fee( sprintf( __('Tip (%s)', 'woocommerce' ), $text ), $fee );
        }
    }
}

Code goes in functions.php file of the active child theme (or active theme). Tested and works.


Addition related to your comment:

To use cart items total including taxes replace in last function:

            // Get cart subtotal excl. Taxes (discounted)
            foreach ( $cart->get_cart() as $cart_item ) {
                $total = $cart_item['line_total'];
            }

with

            // Get cart subtotal Incl. Taxes (discounted)
            foreach ( $cart->get_cart() as $cart_item ) {
                $total = $cart_item['line_total'] + $cart_item['line_tax'];
            }

Leave a Reply

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