Add Free Shipping Progress Bar

Adds a progress bar showing how much more is needed to qualify for free shipping.

Display estimated delivery date
PHP
/**
 * Snippet Name:     Add Free Shipping Progress Bar
 * Snippet Author:   coding-bunny.com
 * Description:      Adds a progress bar showing how much more is needed to qualify for free shipping.
 * Version:          1.2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

add_action( 'init', 'cbfpb_init_hooks' );

function cbfpb_init_hooks() {
    // Product page
    add_action( 'woocommerce_after_add_to_cart_button', 'cbfpb_display_product_bar', 25 );
    
    // Cart page
    add_action( 'woocommerce_cart_totals_after_order_total', 'cbfpb_display_cart_bar' );
    
    // Checkout page
    add_action( 'woocommerce_review_order_after_order_total', 'cbfpb_display_checkout_bar' );
}

function cbfpb_format_price( $amount ) {
    return wc_price( $amount );
}

function cbfpb_currency_symbol() {
    return html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' );
}

function cbfpb_render_bar( $context = 'general' ) {

    if ( ! class_exists( 'WooCommerce' ) ) {
        return;
    }

    $free_shipping_threshold = cbfpb_minimum_amount();
    
    if ( ! $free_shipping_threshold ) {
        return;
    }

    $current_cart_total = 0;
    if ( WC()->cart ) {
        $current_cart_total = WC()->cart->get_subtotal();
    }
    
    $remaining_amount = max( 0, $free_shipping_threshold - $current_cart_total );
    $progress_percentage = min( 100, ( $current_cart_total / $free_shipping_threshold ) * 100 );

    ?>
    <div class="free-shipping-unified <?php echo esc_attr( $context ); ?>-context" 
         data-threshold="<?php echo esc_attr( $free_shipping_threshold ); ?>"
         data-currency="<?php echo esc_attr( cbfpb_currency_symbol() ); ?>"
         data-currency-pos="<?php echo esc_attr( get_option( 'woocommerce_currency_pos' ) ); ?>"
         data-thousand-sep="<?php echo esc_attr( wc_get_price_thousand_separator() ); ?>"
         data-decimal-sep="<?php echo esc_attr( wc_get_price_decimal_separator() ); ?>"
         data-decimals="<?php echo esc_attr( wc_get_price_decimals() ); ?>">
        <?php if ( $remaining_amount > 0 ) : ?>
            <p class="shipping-message">
                <?php
                printf( 
                    esc_html__( 'Add %s more to get free shipping!', 'textdomain' ),
                    wp_kses_post( cbfpb_format_price( $remaining_amount ) )
                );
                ?>
            </p>
        <?php else : ?>
            <p class="shipping-message qualified">
                <?php esc_html_e( 'Free shipping qualified ✓', 'textdomain' ); ?>
            </p>
        <?php endif; ?>
        
        <div class="progress-track">
            <div class="progress-fill" style="width: <?php echo esc_attr( $progress_percentage ); ?>%;"></div>
        </div>
        
        <?php if ( $context === 'checkout' && $remaining_amount == 0 ) : ?>
            <p class="shipping-bonus">
                <?php esc_html_e( 'Free shipping will be applied at final step', 'textdomain' ); ?>
            </p>
        <?php endif; ?>
    </div>
    <?php
}

function cbfpb_display_product_bar() {
    cbfpb_render_bar( 'product' );
}

function cbfpb_display_cart_bar() {
    ?>
    <tr class="free-shipping-progress-row">
        <td colspan="2" class="free-shipping-cell">
            <?php cbfpb_render_bar( 'cart' ); ?>
        </td>
    </tr>
    <?php
}

function cbfpb_display_checkout_bar() {
    ?>
    <tr class="free-shipping-checkout-row">
        <td colspan="2" class="free-shipping-checkout-cell">
            <?php cbfpb_render_bar( 'checkout' ); ?>
        </td>
    </tr>
    <?php
}

function cbfpb_minimum_amount() {
    static $cached_amount = null;
    
    if ( $cached_amount !== null ) {
        return $cached_amount;
    }
    
    $shipping_zones = WC_Shipping_Zones::get_zones();
    $shipping_zones[] = array( 'zone_id' => 0 );
    
    foreach ( $shipping_zones as $zone ) {
        $zone_id = $zone['zone_id'];
        $shipping_zone = new WC_Shipping_Zone( $zone_id );
        $shipping_methods = $shipping_zone->get_shipping_methods( true );
        
        foreach ( $shipping_methods as $method ) {
            if ( 'free_shipping' === $method->id && 'yes' === $method->enabled ) {
                $min_amount = $method->get_option( 'min_amount' );
                
                if ( ! empty( $min_amount ) && is_numeric( $min_amount ) ) {
                    $cached_amount = (float) $min_amount;
                    return $cached_amount;
                }
            }
        }
    }
    
    $cached_amount = false;
    return false;
}

add_action( 'wp_head', 'cbfpb_styles' );

function cbfpb_styles() {
    if ( ! is_product() && ! is_cart() && ! is_checkout() ) {
        return;
    }
    ?>
    <style>
        .free-shipping-unified {
            margin: 0;
            padding: 12px;
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 6px;
            font-size: 14px;
        }
        
        .free-shipping-unified.product-context {
            margin: 0;
        }
        
        .free-shipping-unified.cart-context {
            margin: 0;
        }
        
        .free-shipping-unified.checkout-context {
            margin: 0;
        }
        
        .free-shipping-cell,
        .free-shipping-checkout-cell {
            padding: 15px 0 !important;
            border-top: 1px solid #e0e0e0;
        }
        
        .shipping-message {
            margin: 0 0 8px 0;
            font-weight: 400;
            text-align: center;
            font-size: 14px;
        }
        
        .shipping-message.qualified {
            color: #27ae60;
        }
        
        .shipping-bonus {
            margin: 8px 0 0 0;
            font-size: 12px;
            color: #27ae60;
            text-align: center;
            font-style: italic;
        }
        
        .progress-track {
            height: 6px;
            background: #e0e0e0;
            border-radius: 3px;
            overflow: hidden;
        }
        
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #27ae60 0%, #2ecc71 100%);
            border-radius: 3px;
            transition: width 0.3s ease;
        }
        
        .progress-fill.updating {
            transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
        }
        
        .shipping-message.updating {
            transition: color 0.3s ease;
        }
        
        .shipping-message .woocommerce-Price-amount {
            font-weight: 700;
        }
        
        @media (max-width: 768px) {
            .free-shipping-unified {
                font-size: 13px;
                padding: 10px;
            }
            
            .shipping-message {
                font-size: 13px;
            }
        }
        
        .woocommerce-cart .free-shipping-progress-row,
        .woocommerce-checkout .free-shipping-checkout-row {
            border-top: 1px solid #e0e0e0;
        }
    </style>
    <?php
}

add_action( 'wp_ajax_cbfpb_cart_total', 'cbfpb_ajax_cart_total' );
add_action( 'wp_ajax_nopriv_cbfpb_cart_total', 'cbfpb_ajax_cart_total' );

function cbfpb_ajax_cart_total() {
    if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'cbfpb_nonce' ) ) {
        wp_die( 'Security check failed' );
    }

    if ( ! class_exists( 'WooCommerce' ) || ! WC()->cart ) {
        wp_send_json_error( 'WooCommerce not available' );
    }
    
    $free_shipping_threshold = cbfpb_minimum_amount();
    
    if ( ! $free_shipping_threshold ) {
        wp_send_json_error( 'No free shipping available' );
    }
    
    $current_cart_total = WC()->cart->get_subtotal();
    $remaining_amount = max( 0, $free_shipping_threshold - $current_cart_total );
    $progress_percentage = min( 100, ( $current_cart_total / $free_shipping_threshold ) * 100 );
    
    $response = array(
        'remaining' => $remaining_amount,
        'remaining_formatted' => cbfpb_format_price( $remaining_amount ),
        'percentage' => $progress_percentage,
        'qualified' => $remaining_amount == 0,
        'current_total' => $current_cart_total,
        'threshold' => $free_shipping_threshold
    );
    
    wp_send_json_success( $response );
}

add_action( 'wp_footer', 'cbfpb_realtime_script' );

function cbfpb_realtime_script() {
    if ( ! is_product() && ! is_cart() && ! is_checkout() ) {
        return;
    }
    ?>
    <script>
        jQuery(document).ready(function($) {
            var updateTimeout;
            
            function formatPrice(amount, currency, currencyPos, thousandSep, decimalSep, decimals) {
                var formattedAmount = parseFloat(amount).toFixed(decimals);
                
                if (thousandSep) {
                    var parts = formattedAmount.split('.');
                    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
                    formattedAmount = parts.join(decimalSep);
                } else if (decimalSep !== '.') {
                    formattedAmount = formattedAmount.replace('.', decimalSep);
                }
                
                switch(currencyPos) {
                    case 'left':
                        return currency + formattedAmount;
                    case 'right':
                        return formattedAmount + currency;
                    case 'left_space':
                        return currency + ' ' + formattedAmount;
                    case 'right_space':
                        return formattedAmount + ' ' + currency;
                    default:
                        return currency + formattedAmount;
                }
            }
            
            function updateProgressBarDisplay(data) {
                var $progressFill = $('.progress-fill');
                var $shippingMessage = $('.shipping-message');
                var $progressBar = $('.free-shipping-unified');
                
                $progressFill.addClass('updating');
                $shippingMessage.addClass('updating');
                
                var message;
                if (data.qualified) {
                    message = 'Free shipping qualified ✓';
                } else {

                    if (data.remaining_formatted) {
                        message = 'Add ' + data.remaining_formatted + ' more to get free shipping!';
                    } else {

                        var currency = $progressBar.data('currency');
                        var currencyPos = $progressBar.data('currency-pos');
                        var thousandSep = $progressBar.data('thousand-sep');
                        var decimalSep = $progressBar.data('decimal-sep');
                        var decimals = parseInt($progressBar.data('decimals'));
                        
                        var formattedPrice = formatPrice(data.remaining, currency, currencyPos, thousandSep, decimalSep, decimals);
                        message = 'Add ' + formattedPrice + ' more to get free shipping!';
                    }
                }
                
                $shippingMessage.html(message)
                    .toggleClass('qualified', data.qualified);
                
                $progressFill.css('width', data.percentage + '%');
                
                if ($('.checkout-context').length && data.qualified) {
                    if (!$('.shipping-bonus').length) {
                        $('.checkout-context .progress-track').after(
                            '<p class="shipping-bonus">Free shipping will be applied at final step</p>'
                        );
                    }
                } else {
                    $('.shipping-bonus').remove();
                }
                
                setTimeout(function() {
                    $progressFill.removeClass('updating');
                    $shippingMessage.removeClass('updating');
                }, 500);
            }
            
            function updateShippingProgress() {
                $.post('<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>', {
                    action: 'cbfpb_cart_total',
                    nonce: '<?php echo esc_js( wp_create_nonce( 'cbfpb_nonce' ) ); ?>'
                }, function(response) {
                    if (response.success) {
                        updateProgressBarDisplay(response.data);
                    }
                });
            }
            
            $(document.body).on('added_to_cart', function(event, fragments, cart_hash, button) {
                setTimeout(updateShippingProgress, 100);
            });
            
            $(document.body).on('updated_wc_div', function() {
                updateShippingProgress();
            });
            
            $(document.body).on('updated_cart_totals', function() {
                updateShippingProgress();
            });
            
            $(document.body).on('updated_checkout', function() {
                updateShippingProgress();
            });
            
            $(document).on('input change', 'input.qty', function() {
                clearTimeout(updateTimeout);
                updateTimeout = setTimeout(function() {
                    updateShippingProgress();
                }, 300);
            });
            
            $(document).on('click', 'a.remove', function() {
                setTimeout(updateShippingProgress, 800);
            });
            
            $('form.variations_form').on('found_variation', function() {
                setTimeout(updateShippingProgress, 100);
            });
            
            if ($('.product-context').length) {
                $(document).on('input change', 'input[name="quantity"]', function() {
                    updateShippingProgress();
                });
            }
            
            setInterval(updateShippingProgress, 30000);
            
            setTimeout(updateShippingProgress, 500);
        });
    </script>
    <?php
}
?>

How To Implement This Solution?

Leave a Reply