phpwoocommercehook-woocommerce

Unable to select shipping method after filtering 'woocommerce_package_rates'


I just wrote this filter to disable non-free shipping methods when free shipping is available:

add_filter( 'woocommerce_package_rates', 'disable_paid_shipping', 9999, 2 );
function disable_paid_shipping( $rates, $package ) {
    $free_rates = array();

    foreach ( $rates as $i => $rate ) {
        if ( str_contains( $rate->label, "gratuita" ) OR str_contains( $rate->label, "gratuito" ) ) {
            $free_rates[] = $rate;
        }
    
        if ( str_contains( $rate->id, "local") ) {
            $local = $rate;
        }
    
        if ( str_contains( $rate->id, "fermopoint") ) {
            $fermopoint = $rate;
        }
     }
    
    if ( !empty( $free_rates ) ) {
        if ( isset($fermopoint) ) {
            $fermopoint->cost = 0;
            $fermopoint->label .= ' gratuito';
            array_unshift( $free_rates, $fermopoint );
        }

        if ( isset($local) ) {
            $free_rates[] = $local;
        }

        $rates = $free_rates;
    } 

    return $rates;
}

The code works as expected, unless for two unexpected events occurring:

  1. no shipping method is selected by default anymore (both in cart and checkout page)
  2. when I choose one in the cart page, it gets unselected right after (only in cart page)

To solve the 1st problem at checkout, I can work around by forcing the selection through a hook on woocommerce_before_cart (although this looks like a forced trick).

For the 2nd problem I have no idea.

Suggestions?


Solution

  • The problem lies in that the WC $rates array is an associative array:

    $rates = Array ( 
                  [rate_id_0] => [rate_obj_0] 
                  [rate_id_1] => [rate_obj_1]
                  ... 
             );
    

    While the newly created $free_rates is an indexed array:

    $free_rates = Array ( 
                  [0] => [rate_obj_0] 
                  [1] => [rate_obj_1] 
                  ... 
             );
    

    As result, WC is unable to match the new $free_rates array with the user's default rate_id, which is supposed to be used as the array key.

    Here's the working code:

    add_filter( 'woocommerce_package_rates', 'disable_paid_shipping', 9999, 2 );
    function disable_paid_shipping( $rates, $package ) {
        $free_rates = array();
    
        foreach ( $rates as $i => $rate ) {
            if ( str_contains( $rate->label, "gratuita" ) OR str_contains( $rate->label, "gratuito" ) ) {
                $free_rates[$rate->id] = $rate;
            }
        
            if ( str_contains( $rate->id, "local") ) {
                $local = $rate;
            }
        
            if ( str_contains( $rate->id, "fermopoint") ) {
                $fermopoint = $rate;
            }
         }
        
        if ( !empty( $free_rates ) ) {
            if ( isset($fermopoint) ) {
                $fermopoint->cost = 0;
                $fermopoint->label .= ' gratuito';
                $free_rates = array($fermopoint->id => $fermopoint) + $free_rates; // to set it as 1st
            }
    
            if ( isset($local) ) {
                $free_rates[$local->id] = $local;
            }
    
            $rates = $free_rates;
        } 
    
        return $rates;
    }