Let’s look at how to put product addon functionality into WooCommerce. I’m doing this in a custom plugin but it will work if you just put the code into the functions file or another included file in your active theme.

If you are going to do this in your theme, you should set up a child theme otherwise you will lose your changes when you update your theme.

The product addon will look like this, and the data for it will flow all the way through to the admin order screen and the emails to the customer and store owner:

If you want to add product addons by category using a custom WooCommerce Settings Tab instead of on individual products, check out this post!

Functionality – putting a product addon on the WooCommerce product page

Let’s say we want to offer our customers the option to add a $2 scratchy to their purchase. We want to do it on both single and variable products – but not all of them. Sometimes we want to turn it off. We don’t want to put it on downloadable products for example. So we need a checkbox on the backend that can be used to conditionally add the product addon to the product.

I’m gonna use two of the WC sample products to demonstrate this: the hoodie which is variable and the sunglasses which is simple.

Putting a checkbox on the backend of a product

There are hooks that can be used just for simple or just for variable products, but I want to add this to both types with one hook so I’m going to use the woocommerce_product_options_general_product_data hook. It works like this:

add_action( 'woocommerce_product_options_general_product_data', 'clario_add_product_addon_to_product' );
 
function clario_add_product_addon_to_product(  ) {

  echo '<div class="options_group">'; 
    woocommerce_wp_checkbox( array(
      'id'          => '_clario_two_dollar_scratchy_product_addon',
      'value'       => get_post_meta( get_the_ID(), '_clario_two_dollar_scratchy_product_addon' )[0],
      'label'       => __( '$2 scratchy Product Addon', 'woocommerce' ),
      'description' => __( 'Add $2 scratchy to this product', 'woocommerce' ),
    ) );
   echo '</div>';
}

You should preface your functions – I’ve put ‘clario’ at the front of this one in the hope that no other plugins on my site have the same function name as what I’m using. You will get a fatal error if you end up using the same function name as another function.

Now I’ve got a checkbox on the back end in the General tab of the simple product:

And the general tab of the variable product:

Checkbox on the backend of a variable WooCommerce product in the general tab. Done with the woocommerce_product_options_general_product_data hook

A few notes about the above function:

The function I’m calling to output a checkbox is woocommerce_wp_checkbox. It only works on the backend. You can’t use it to put a check box on the front end of a product. We’ll see what you have to do to do that later.

The function takes an array. One of the array elements is ‘id’. I’ve set this to ‘_clario_two_dollar_scratchy_product_addon’.

'id' => '_clario_two_dollar_scratchy_product_addon',

It should be unique, like all IDs.

I’m using that ID again in the next line which is the value element of the array:

'value' => get_post_meta( get_the_ID(), '_clario_two_dollar_scratchy_product_addon' )[0],

The value element is used to set the checkbox when it’s loaded – if I checked it before and I load the product now, I expect it still to be checked. So the woocommerce_wp_checkbox function takes a value that tells it whether or not to check the checkbox.

I’m using get_post_meta to get that value. get_post_meta gets a value from the wp_postmeta table of the database. We’ll set that value in the next hook.

get_post_meta takes a post ID (in this case a product ID) and a meta key (in this case the ID that we’ve set to our checkbox in the previous line). And since it’s returning an array, I’m using [0] to get the first element of that array. So it’s saying, what is the value of the _clario_two_dollar_scratchy_product_addon field for the product with a certain ID.

The label and description elements come up on the admin screen for the product which you can see in the images.

Checkbox with label and description on WooCommerce product back end

Saving the value of the checkbox in the database

If you try clicking on the checkbox and then update the product you’ll see that the checkbox doesn’t stay checked. That’s because although we are reading the value of the _clario_two_dollar_scratchy_product_addon meta data field, we’re not setting it, so the checkbox is just getting an empty value. We now need to save its value to the database. The woocommerce_process_product_meta hook will do this for both simple and variable products neatly:

add_action( 'woocommerce_process_product_meta', 'clario_save_two_dollar_scratchy_product_addon_meta', 10, 2 );

function clario_save_two_dollar_scratchy_product_addon_meta( $id, $post ){
  update_post_meta( $id, '_clario_two_dollar_scratchy_product_addon', $_POST['_clario_two_dollar_scratchy_product_addon'] );
}

This hook fires when the product is updated in the backend. Here, I’m using the $_POST variable to get the value of the form submission from the WooCommerce admin screen. I’m using update_post_meta to set the value for the _clario_two_dollar_scratchy_product_addon meta field for the product. update_post_meta is a companion function to get_post_meta – one sets, one gets.

You’ll be able to see this data in the database now if you execute this bit of SQL:

select    * 
from      wp_postmeta 
where     meta_key = '_clario_two_dollar_scratchy_product_addon';

So now we know whether a product should show the $2 scratchy product addon and that info is stored in the database. What next? We need to actually show it on the front end.

Showing the Product Addon Checkbox on the Front End

I’m going to show the checkbox before the add to cart button using the woocommerce_before_add_to_cart_button hook. This hook is referring to a location in the front end product screen just before the add to cart button. You could put the checkbox in a few places and there are a lot of good resources to figure out where.

add_action( 'woocommerce_before_add_to_cart_button', 'clario_two_dollar_scratchy_show_addon_product' );

function clario_two_dollar_scratchy_show_addon_product() {

  global $product;
  $showAddon = $product->get_meta( '_clario_two_dollar_scratchy_product_addon' );

  if ($showAddon == 'yes') {
    echo  
    '<div class="product-addon-checkbox">
      <input type="checkbox" 
             id="clario_two_dollar_scratchy_product_addon" 
             name="clario_two_dollar_scratchy_product_addon" 
             value="YES">
      <label for="clario_two_dollar_scratchy_product_addon">
        <strong>Add a $2 scratchy to your purchase?</strong>
      </label>
    </div>';
  }
}

Now we have a checkbox showing up on the products, woohoo!

WooCommerce product with product addon checkbox showing on the front end

I’ve put a little bit of styling on this to make it look better:

.product-addon-checkbox {
    padding: 25px 15px;
    border: solid 1px lightgrey;
    margin-bottom: 20px;
}
.product-addon-checkbox label {
    padding-left: 5px;
}

A bit about the above function. It’s using the WooCommerce $product global variable. Like all WP globals you can call it by using the global keyword. It has a get_meta method which I’m using to get the value of the _clario_two_dollar_scratchy_product_addon meta field. I don’t need to pass it an ID in this case because the method is on the product itself, so it already knows its ID:

global $product;
$showAddon = $product->get_meta( '_clario_two_dollar_scratchy_product_addon' );

Then I am conditionally showing the checkbox based on the variable $showAddon. The checkbox is just done in html and css. The div I’ve created is part of the form on the product page which is why I’ll be able to use the value of the check box later when the form submits.

So we’re kinda half way there. Now we need to handle the add to cart scenario.

Adding the Product to the Cart

When someone adds the product to their cart with the checkbox checked we need to add the cost of the addon to the cart item cost and add a message to the product to say that’s what we’ve done.

To do this, we’ll set a flag on the cart data that we’ll use in later hooks to conditionally change the product price and add the message on the cart and checkout. We’ll use the woocommerce_add_cart_item_data to set the flag:

add_filter( 'woocommerce_add_cart_item_data', 'clario_add_addon_to_cart_item', 99, 2 );
function clario_add_addon_to_cart_item( $cart_item_data, $product_id ) {

  if( isset( $_POST['clario_two_dollar_scratchy_product_addon'] ) 
    && $_POST['clario_two_dollar_scratchy_product_addon'] === 'YES' ) {
      $cart_item_data[ 'clario_two_dollar_scratchy_product_addon' ] = 'YES';
      unset($_POST['clario_two_dollar_scratchy_product_addon']); 
      // unset so this only gets applied to the main product 
  }
  return $cart_item_data;
     
}

This hook fires when the form on the front end product page submits which is why I can once again use the $_POST variable to get the value of the checkbox. If it’s checked, I’m setting the flag on the $cart_item_data variable:

$cart_item_data[ 'clario_two_dollar_scratchy_product_addon' ] = 'YES';

I’ve also unset the value in the $_POST variable in this line:

unset($_POST['clario_two_dollar_scratchy_product_addon']);

I’ve done this because I don’t want the addon to be applied to any other product addons that I might have on the page.

It’s not something that would be a problem with my current product, but let’s say we were selling garage doors and every product page had an optional addon for installation. The installation product is a real WooCommerce product that is added via a checkbox and some custom code on the product page. If someone buys a garage door, adds a scratchy, also checks the installation product and then clicks the add to cart button, the woocommerce_add_cart_item_data hook is going to fire for both the garage door and the installation product. (note, it is not firing for the scratchy addon because that is not a true product, but just a checkbox/flag that we are using to add data). I only want to add the scratchy to the garage door – the primary product – so when the woocommerce_add_cart_item_data hook fires for the installation product, I need the $_POST[‘clario_two_dollar_scratchy_product_addon’] field to no longer be set.

Updating the product price

To set the product price we can use the woocommerce_before_calculate_totals hook:

add_action( 'woocommerce_before_calculate_totals', 'clario_add_two_dollar_scratchy_calculate_totals', 99 );
function clario_add_two_dollar_scratchy_calculate_totals( $cart_object ) {

  if( !WC()->session->__isset( 'reload_checkout' )) {
    foreach ( WC()->cart->get_cart() as $key => $value ) {
      if( isset( $value['clario_two_dollar_scratchy_product_addon'] ) ) {                
        $price = floatval( $value['data']->get_price() );
        $value['data']->set_price( $price + 2 );       
      }
    }
  }
}

This function is looping through the items in the cart and checking if the clario_two_dollar_scratchy_product_addon flag that we set in the previous hook is set. If it is, the code gets the price and then resets it with $2 added. Phew, the easiest one yet.

Adding a message to the cart and checkout pages

To add a message to the cart and checkout pages we can use a single hook. That hook is woocommerce_get_item_data:

add_filter( 'woocommerce_get_item_data', 'clario_add_two_dollar_scratchy_cart_checkout', 99, 2 );
function clario_add_two_dollar_scratchy_cart_checkout( $cart_data, $cart_item = null ) {

  if( isset( $cart_item['clario_two_dollar_scratchy_product_addon'] ) ) {
    $cart_data['scratchy'] = array(
      'name' => __( '$2 scratchy added', 'woocommerce' ),
      'value' => '$2'
    );
  }
  return $cart_data;
}

Now we can see the product addon in the cart:

WooCommerce product with addon in cart with price increase by the cost of the product addon

You’ll notice now too that if you were to change the number of items in the cart and then update it, the price is preserved eg, the $2 is added again (now the cart contains 2 pairs of $90 sunglasses and 2 $2 scratchies – a total of $184):

When you increase the number of items in the cart, the product addon is also added again

Preserving Variable Product Cart Data

You’ll see the woocommerce_get_item_data hook used a lot and often it’s just used to set a value and return it which then gets appended to the cart item as a message. Something like this:

  $meta_items = array();
  if( isset( $cart_item['clario_two_dollar_scratchy_product_addon'] ) ) {
    $meta_items[] = array( 'name' => '$2 Scratchy added', 'value' => '$2' );
  }
  return $meta_items;

That’s OK if you don’t have other info already appended to the product. But in my case there is extra data already in the variable product (eg, color, logo, brand):

WooCommerce Variable Product in cart with data about the variation

If I just set a value here, all that data will get clobbered in favour of whatever I set the value to. So I need to append my message to the cart info. To do that, instead of just passing a value back from the function, I’ve created a new $cart_data element and passed back the whole $cart_data parameter:

if( isset( $cart_item['clario_two_dollar_scratchy_product_addon'] ) ) {
  $cart_data['scratchy'] = array( // add a new element to the cart data
    'name' => __( '$2 scratchy added', 'woocommerce' ),
    'value' => '$2'
  );
}
return $cart_data;

Now when I look at the product in the cart, I get the extra data, plus my scratchy message:

WooCommerce variable product with extra data preserved and product addon info appended

And here’s the price and the message carried over to the cart page:

WooCommerce variable product in checkout page with extra data preserved and product addon info appended

Adding info to the thank you & admin order screens and the order emails

If you’re still with me, this is the last thing we’ve got to do. We can use the woocommerce_add_order_item_meta to do this and you’ll see it a lot online, however that function is now deprecated. We’re going to use woocommerce_checkout_create_order_line_item instead, which is functionally similar and not deprecated! Yay!

add_action( 'woocommerce_checkout_create_order_line_item', 'clario_two_dollar_scratchy_order_meta', 20, 4 );

function clario_two_dollar_scratchy_order_meta( $item, $cart_item_key, $values, $order ) {
  if ( isset( $values['clario_two_dollar_scratchy_product_addon'] ) ) {         
    $item->update_meta_data( __('$2 scratchy added', 'woocommerce'), '$2' );          
  }
}

This hook is adding a line item to the order object using the update_meta_data of that item. WooCommerce will then display that line item along with any others the product might have on the thank you page, the admin order page and in the emails to the customer and the store owner. It’s that simple.

Here’s the price and the message carried over to the thank you page:

Product Addon message and price in WooCommerce thankyou page

And on the admin order page:

Hey there – thanks for reading!
Have I helped you? If so, could you…

Acknowledgements

There are a few fellow programmers whose posts and Stack Overflow answers helped me figure out how to do this. They are Loic The Aztec, Misha Rudrastyh and  Saravana Kumar K. Follow them, they’re ace!

The Full Code

And here’s the full code:

<?php

add_action( 'woocommerce_product_options_general_product_data', 'clario_add_product_addon_to_product' );
function clario_add_product_addon_to_product(  ) {

  echo '<div class="options_group">'; 
    woocommerce_wp_checkbox( array(
      'id'            => '_clario_two_dollar_scratchy_product_addon',
      'value'         => get_post_meta( get_the_ID(), '_clario_two_dollar_scratchy_product_addon' )[0],
      'label'         => __( '$2 scratchy Product Addon', 'woocommerce' ),
      'description'   => __( 'Add $2 scratchy to this product?', 'woocommerce' ),
    ) );
   echo '</div>';
}

add_action( 'woocommerce_process_product_meta', 'clario_save_2_dollar_scratchy_product_addon_meta', 10, 2 );
function clario_save_2_dollar_scratchy_product_addon_meta1( $id, $post ){
  update_post_meta( $id, '_clario_two_dollar_scratchy_product_addon', $_POST['_clario_two_dollar_scratchy_product_addon'] );
}

add_action( 'woocommerce_before_add_to_cart_button', 'clario_two_dollar_scratchy_show_addon_product' );
function clario_two_dollar_scratchy_show_addon_product() {

  global $product;
  $showAddon = $product->get_meta( '_clario_two_dollar_scratchy_product_addon' );

  if ($showAddon == 'yes') {
    echo  
    '<div class="product-addon-checkbox">
      <input type="checkbox" id="clario_two_dollar_scratchy_product_addon" name="clario_two_dollar_scratchy_product_addon" value="YES">
      <label for="clario_two_dollar_scratchy_product_addon"><strong>Add a $2 scratchy to your purchase?</strong></label>
    </div>';
  }
}

add_filter( 'woocommerce_add_cart_item_data', 'clario_add_addon_to_cart_item', 99, 2 );
function clario_add_addon_to_cart_item( $cart_item_data, $product_id ) { 

  if( isset( $_POST['clario_two_dollar_scratchy_product_addon'] ) && $_POST['clario_two_dollar_scratchy_product_addon'] === 'YES' ) {
    $cart_item_data[ 'clario_two_dollar_scratchy_product_addon' ] = 'YES';
    unset($_POST['clario_two_dollar_scratchy_product_addon']); // only want to apply this to the main product and not the installation addon
  }
  return $cart_item_data;
     
}

add_action( 'woocommerce_before_calculate_totals', 'clario_add_two_dollar_scratchy_calculate_totals', 99 );
function clario_add_two_dollar_scratchy_calculate_totals( $cart_object ) {

  if( !WC()->session->__isset( 'reload_checkout' )) {
    foreach ( WC()->cart->get_cart() as $key => $value ) {
      if( isset( $value['clario_two_dollar_scratchy_product_addon'] ) ) {                
        $price = floatval( $value['data']->get_price() );
        $value['data']->set_price( $price + 2 );       
      }
    }
  }
}

add_filter( 'woocommerce_get_item_data', 'clario_add_two_dollar_scratchy_cart_checkout', 99, 2 );
function clario_add_two_dollar_scratchy_cart_checkout( $cart_data, $cart_item = null ) {

  if( isset( $cart_item['clario_two_dollar_scratchy_product_addon'] ) ) {
    $cart_data['scratchy'] = array(
      'name' => __( '$2 scratchy added', 'woocommerce' ),
      'value' => '$2'
    );
  }
  return $cart_data;
}

add_action( 'woocommerce_checkout_create_order_line_item', 'clario_two_dollar_scratchy_order_meta', 20, 4 );
function clario_two_dollar_scratchy_order_meta( $item, $cart_item_key, $values, $order ) {

  if ( isset( $values['clario_two_dollar_scratchy_product_addon'] ) ) {         
    $item->update_meta_data( __('$2 scratchy added', 'woocommerce'), '$2' );          
  }
}

Finally, this stuff is pretty hard. If you notice any bugs, security issues or functions that are deprecated in my code, please let me know in the comments. And if what I’ve written has helped you and you want to say thanks, you can buy me a beer.

Add a Product Addon Checkbox to WooCommerce Single & Variable Products – Tutorial

Leave a Reply

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