Add custom gift option
August 7, 2025 · AI Conversations
—
### 🧩 Scenario:
You want to show a checkbox:
☑️ “Add a Free Gift (Custom Product)”
If checked, a **custom product (e.g., gift)** is added to the cart dynamically **without page reload**, using AJAX.
—
## ✅ Step-by-Step Implementation
—
### 1. **Define the Free Gift Product ID**
Add this to `functions.php` or a custom plugin:
function get_custom_gift_product_id() {
return 1234; // Replace with actual Product ID of the free gift
}
—
### 2. **Show the Gift Checkbox on Cart Page**
Add this to `functions.php`:
add_action('woocommerce_cart_coupon', 'add_free_gift_checkbox_on_cart');
function add_free_gift_checkbox_on_cart() {
$gift_product_id = get_custom_gift_product_id();
$is_in_cart = false;
foreach (WC()->cart->get_cart() as $cart_item) {
if ($cart_item['product_id'] == $gift_product_id) {
$is_in_cart = true;
break;
}
}
?>
<div style="margin-top:20px;" id="custom-gift-checkbox">
<label>
<input type="checkbox" id="add_free_gift" <?php checked($is_in_cart); ?> />
Add a Free Gift to Your Cart
</label>
<span class="ajax-loader" style="display:none;">Adding...</span>
</div>
<script type="text/javascript">
jQuery(function($) {
$('#add_free_gift').on('change', function() {
$('.ajax-loader').show();
$.ajax({
url: '<?php echo admin_url('admin-ajax.php'); ?>',
method: 'POST',
data: {
action: 'toggle_free_gift',
checked: $(this).is(':checked') ? 1 : 0
},
success: function(response) {
location.reload(); // Refresh cart to update totals
}
});
});
});
</script>
<?php
}
—
### 3. **Handle the AJAX Request**
Add to `functions.php`:
add_action('wp_ajax_toggle_free_gift', 'ajax_add_remove_free_gift');
add_action('wp_ajax_nopriv_toggle_free_gift', 'ajax_add_remove_free_gift');
function ajax_add_remove_free_gift() {
$gift_product_id = get_custom_gift_product_id();
if (isset($_POST['checked']) && $_POST['checked']) {
// Add gift if not already in cart
$exists = false;
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if ($cart_item['product_id'] == $gift_product_id) {
$exists = true;
break;
}
}
if (!$exists) {
WC()->cart->add_to_cart($gift_product_id, 1);
}
} else {
// Remove gift if exists
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if ($cart_item['product_id'] == $gift_product_id) {
WC()->cart->remove_cart_item($cart_item_key);
break;
}
}
}
wp_send_json_success();
}
—
### ✅ Result
– Users see a checkbox to add a gift item.
– On checking it, the item is added to cart via AJAX.
– Unchecking removes it.
– Page refreshes automatically to update the cart.
—
### 🛠️ Optional Improvements
– Add a loading spinner instead of text `Adding…`
– Prevent manually removing the gift item (by hiding its remove link)
– Style the checkbox with custom CSS
Would you like any of these enhancements too?
—
## ✅ Goal
– Checkbox to add/remove a gift item
– Gift item gets added/removed via AJAX
– Cart totals and cart table update **without page reload**
—
## 🔧 Full Implementation
—
### 1. **Add Checkbox to Cart Page**
In `functions.php`:
add_action('woocommerce_cart_coupon', 'add_free_gift_checkbox_on_cart');
function add_free_gift_checkbox_on_cart() {
$gift_product_id = get_custom_gift_product_id();
$is_in_cart = false;
foreach (WC()->cart->get_cart() as $cart_item) {
if ($cart_item['product_id'] == $gift_product_id) {
$is_in_cart = true;
break;
}
}
?>
<div id="custom-gift-checkbox" style="margin-top: 20px;">
<label>
<input type="checkbox" id="add_free_gift" <?php checked($is_in_cart); ?> />
Add a Free Gift to Your Cart
</label>
<span class="ajax-loader" style="display:none;">Processing...</span>
</div>
<script type="text/javascript">
jQuery(function($) {
$('#add_free_gift').on('change', function() {
$('.ajax-loader').show();
$.ajax({
url: wc_cart_params.ajax_url,
type: 'POST',
data: {
action: 'toggle_free_gift',
checked: $(this).is(':checked') ? 1 : 0,
},
success: function(response) {
if (response.success) {
$(document.body).trigger('update_checkout'); // for checkout page if needed
$(document.body).trigger('wc_fragment_refresh'); // refresh cart fragments
}
$('.ajax-loader').hide();
}
});
});
});
</script>
<?php
}
—
### 2. **Add the Gift Product (AJAX Logic)**
Also in `functions.php`:
function get_custom_gift_product_id() {
return 1234; // Change this to your actual gift product ID
}
add_action('wp_ajax_toggle_free_gift', 'ajax_add_remove_free_gift');
add_action('wp_ajax_nopriv_toggle_free_gift', 'ajax_add_remove_free_gift');
function ajax_add_remove_free_gift() {
$gift_product_id = get_custom_gift_product_id();
if (isset($_POST['checked']) && $_POST['checked']) {
// Add to cart if not present
$exists = false;
foreach (WC()->cart->get_cart() as $cart_item) {
if ($cart_item['product_id'] == $gift_product_id) {
$exists = true;
break;
}
}
if (!$exists) {
WC()->cart->add_to_cart($gift_product_id, 1);
}
} else {
// Remove from cart
foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
if ($cart_item['product_id'] == $gift_product_id) {
WC()->cart->remove_cart_item($cart_item_key);
break;
}
}
}
WC_AJAX::get_refreshed_fragments(); // Respond with updated cart fragments
wp_die();
}
—
### 3. **Make Sure `wc_cart_fragments` Is Enqueued**
WooCommerce loads it automatically, but double check with:
add_action('wp_enqueue_scripts', function () {
if (is_cart()) {
wp_enqueue_script('wc-cart-fragments');
}
});
—
### 4. ✅ Result
– ✅ No page reload
– ✅ Gift item appears/disappears dynamically
– ✅ Cart totals and table update instantly
– ✅ Works on Cart and Checkout pages
—
### 🔄 Bonus: Hide “Remove” Link on Gift (Optional)
add_filter('woocommerce_cart_item_remove_link', function($link, $cart_item_key) {
$gift_id = get_custom_gift_product_id();
$cart = WC()->cart->get_cart();
if (isset($cart[$cart_item_key]['product_id']) && $cart[$cart_item_key]['product_id'] == $gift_id) {
return ''; // Don't show remove link for gift
}
return $link;
}, 10, 2);
—
Would you like to make the gift **free only when cart total is above certain amount**, or **limit it to once per user**?
// if( isset( $_POST['action_type'] ) && $_POST['action_type'] == 'add' ) {
// $this->pfc_remove_last_gift();
// }
if( isset( $_POST['action_type'] ) && $_POST['action_type'] == 'add' ) {
if( is_numeric( $_POST['prd_id'] ) ) {
if( isset( $_POST['type'] ) && $_POST['type'] ) {
$this->remove_cart_products_by_type( $_POST['type'] );
}
WC()->cart->add_to_cart( $_POST['prd_id'] );
}
update_user_meta( get_current_user_id(), 'pfc_last_added_gift', $_POST['prd_id'] );
}
elseif( isset( $_POST['action_type'] ) && $_POST['action_type'] == 'remove' && is_numeric( $_POST['prd_id'] ) ) {
$product_id = $_POST['prd_id'];
$product_cart_id = WC()->cart->generate_cart_id( $product_id );
$cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
if( $cart_item_key ) {
WC()->cart->remove_cart_item( $cart_item_key );
}
}
// apply_filters( 'woocommerce_update_cart_action_cart_updated', true );
// Calculate totals and get the cart fragments
WC()->cart->calculate_totals();
// Get the updated fragments
$fragments = [];
ob_start();
?>
1. Add the gift product to the cart or remove it via AJAX.
2. Properly **trigger WooCommerce’s fragment updates** using `WC_AJAX::get_refreshed_fragments()`.
—
Here’s your **updated and cleaned-up function**:
public function pfc_gift_add_to_cart() {
$product_id = isset($_POST['prd_id']) ? intval($_POST['prd_id']) : 0;
$action_type = sanitize_text_field($_POST['action_type'] ?? '');
$type = sanitize_text_field($_POST['type'] ?? '');
if ($action_type === 'add' && $product_id) {
if (!empty($type)) {
$this->remove_cart_products_by_type($type); // custom method
}
WC()->cart->add_to_cart($product_id);
update_user_meta(get_current_user_id(), 'pfc_last_added_gift', $product_id);
} elseif ($action_type === 'remove' && $product_id) {
$product_cart_id = WC()->cart->generate_cart_id($product_id);
$cart_item_key = WC()->cart->find_product_in_cart($product_cart_id);
if ($cart_item_key) {
WC()->cart->remove_cart_item($cart_item_key);
}
}
// Recalculate totals
WC()->cart->calculate_totals();
// Output WooCommerce cart fragments (table, totals, mini cart etc.)
WC_AJAX::get_refreshed_fragments();
wp_die(); // Always call wp_die() in AJAX
}
—
## ✅ What This Does:
– Handles both `add` and `remove` actions.
– Supports removing existing gifts of the same type (if passed).
– Returns **all live cart fragments**, including:
– Updated mini cart
– Updated totals
– Updated cart table
– Avoids manual `echo json_encode`, which is fragile and incomplete.
—
## 📝 JS Example to Handle It:
Here’s a sample jQuery AJAX call that works with the above:
jQuery(document).ready(function ($) {
$('#gift-checkbox').on('change', function () {
let isChecked = $(this).is(':checked');
let productId = $(this).data('product-id');
let type = $(this).data('gift-type'); // optional
$.ajax({
url: wc_cart_params.ajax_url,
type: 'POST',
data: {
action: 'pfc_gift_add_to_cart',
action_type: isChecked ? 'add' : 'remove',
prd_id: productId,
type: type
},
success: function (response) {
// WooCommerce fragments are auto refreshed by WC_AJAX::get_refreshed_fragments
console.log('Gift item updated.');
}
});
});
});
—
Would you also like to:
– Prevent adding the same gift twice?
– Limit to one gift per user?
– Add conditional logic (e.g. cart total > X)?
### 🔍 Root Cause
The `[woocommerce_cart]` shortcode **renders the full cart page only during the initial page load**.
Even if you return it via AJAX (like in your earlier `echo json_encode(['cart' => do_shortcode(…)]);`), it won’t **magically replace the live cart table** unless you also **replace that HTML in the DOM manually**.
—
## ✅ ✅ Working Solution
Here’s how to **dynamically update the cart page content (table + totals)** when you add/remove the gift via AJAX:
—
### ✅ 1. Keep Your PHP AJAX Handler Like This:
Keep this version (no need to return the shortcode):
public function pfc_gift_add_to_cart() {
$product_id = isset($_POST['prd_id']) ? intval($_POST['prd_id']) : 0;
$action_type = sanitize_text_field($_POST['action_type'] ?? '');
$type = sanitize_text_field($_POST['type'] ?? '');
if ($action_type === 'add' && $product_id) {
if (!empty($type)) {
$this->remove_cart_products_by_type($type);
}
WC()->cart->add_to_cart($product_id);
update_user_meta(get_current_user_id(), 'pfc_last_added_gift', $product_id);
} elseif ($action_type === 'remove' && $product_id) {
$product_cart_id = WC()->cart->generate_cart_id($product_id);
$cart_item_key = WC()->cart->find_product_in_cart($product_cart_id);
if ($cart_item_key) {
WC()->cart->remove_cart_item($cart_item_key);
}
}
WC()->cart->calculate_totals();
WC_AJAX::get_refreshed_fragments(); // this includes mini-cart, totals, etc.
wp_die();
}
—
### ✅ 2. Update JS to Refresh Full Cart Table via AJAX
You must manually reload the cart table **using another AJAX call to fetch `[woocommerce_cart]`**, and replace the table HTML.
Here’s the updated **JavaScript**:
jQuery(document).ready(function ($) {
$('#gift-checkbox').on('change', function () {
let isChecked = $(this).is(':checked');
let productId = $(this).data('product-id');
let giftType = $(this).data('gift-type') || '';
$.ajax({
url: wc_cart_params.ajax_url,
type: 'POST',
data: {
action: 'pfc_gift_add_to_cart',
action_type: isChecked ? 'add' : 'remove',
prd_id: productId,
type: giftType
},
success: function () {
// Trigger WooCommerce fragment refresh (totals, mini cart)
$(document.body).trigger('wc_fragment_refresh');
// 👇 Replace full cart content manually
$.ajax({
url: window.location.href, // cart page
type: 'GET',
success: function (data) {
const newCartTable = $(data).find('.woocommerce-cart-form'); // replace cart table
const newTotals = $(data).find('.cart_totals');
$('.woocommerce-cart-form').replaceWith(newCartTable);
$('.cart_totals').replaceWith(newTotals);
}
});
}
});
});
});
—
### ✅ What This Fixes
– Keeps your cart page in place without full reload
– Dynamically replaces cart table and totals
– Works with **any cart item changes via AJAX**
– Uses `WC_AJAX::get_refreshed_fragments()` for totals and mini cart
—
Let me know if your theme uses a custom cart layout (e.g. Avada, Divi, Elementor), and I’ll adjust selectors accordingly.
—
### ✅ You need two things:
1. Your PHP AJAX must **handle cart add/remove**, which you're already doing ✅
2. Your JS must:
– Trigger that AJAX call ✅
– **Then fetch the updated cart table HTML** from the cart page
– **Replace** the `.woocommerce-cart-form` element dynamically
—
## ✅ Final Working Solution
—
### 🔧 Step 1: Use this **PHP Handler** (you already have it, just make sure it's like this)
public function pfc_gift_add_to_cart() {
$product_id = isset($_POST['prd_id']) ? intval($_POST['prd_id']) : 0;
$action_type = sanitize_text_field($_POST['action_type'] ?? '');
$type = sanitize_text_field($_POST['type'] ?? '');
if ($action_type === 'add' && $product_id) {
if (!empty($type)) {
$this->remove_cart_products_by_type($type);
}
WC()->cart->add_to_cart($product_id);
update_user_meta(get_current_user_id(), 'pfc_last_added_gift', $product_id);
} elseif ($action_type === 'remove' && $product_id) {
$product_cart_id = WC()->cart->generate_cart_id($product_id);
$cart_item_key = WC()->cart->find_product_in_cart($product_cart_id);
if ($cart_item_key) {
WC()->cart->remove_cart_item($cart_item_key);
}
}
WC()->cart->calculate_totals();
// Return success (we’ll reload cart table from frontend)
wp_send_json_success();
}
✅ No need to return fragments or shortcode here — we'll handle that on frontend.
—
### 🧠 Step 2: JavaScript to update cart table & totals
Add this in your JS file (or inline, e.g., in `woocommerce_cart_coupon` hook):
jQuery(function ($) {
$('#gift-checkbox').on('change', function () {
let isChecked = $(this).is(':checked');
let productId = $(this).data('product-id');
let giftType = $(this).data('gift-type') || '';
$.ajax({
url: wc_cart_params.ajax_url,
type: 'POST',
data: {
action: 'pfc_gift_add_to_cart',
action_type: isChecked ? 'add' : 'remove',
prd_id: productId,
type: giftType
},
success: function (response) {
if (response.success) {
// 🔁 Fetch updated cart page HTML (we need only table)
$.get(window.location.href, function (data) {
const updatedCartTable = $(data).find('.woocommerce-cart-form');
const updatedCartTotals = $(data).find('.cart_totals');
$('.woocommerce-cart-form').replaceWith(updatedCartTable);
$('.cart_totals').replaceWith(updatedCartTotals);
});
}
}
});
});
});
—
### ✅ Example HTML Checkbox in Cart Page
Add this via `woocommerce_cart_coupon` or manually in template:
<input type="checkbox" id="gift-checkbox"
data-product-id="1234"
data-gift-type="gift_type"
<?php checked( WC()->cart->find_product_in_cart( WC()->cart->generate_cart_id(1234) ) ); ?> />
<label for="gift-checkbox">Add a Free Gift</label>
Replace `1234` with your gift product ID.
—
## ✅ Result
– Add/remove gift via checkbox
– AJAX hits your handler
– JS then reloads `.woocommerce-cart-form` and `.cart_totals` from the current cart page
– No full page reload needed 🚀
—
Let me know if you're using a **custom WooCommerce theme** or **Ajax-based cart page** — I’ll adjust selectors and compatibility for that.
$.post(global_object.ajax_url, data, function (resp) {
// if( $( 'button[name="update_cart"]' ).length > 0 && '' ) {
// $( 'button[name="update_cart"]' ).removeAttr( 'disabled' );
// $( 'button[name="update_cart"]' ).trigger( 'click' );
// }
if ($is_cart && resp.cart && $('.content-area.page-wrapper .woocommerce')) {
resp.cart = resp.cart.replace(/pfc-checks/g, '');
if ($('.pfc-temp-cart').length
– Handle replacement of cart contents properly
– Use `.html()` correctly (it needs `.html(content.html())` not `.html(content)`)
– Ensure compatibility with WooCommerce cart page
– Remove unnecessary commented blocks
– Add error safety and smoother handling
—
### ✅ Updated JS (clean + functional)
$.post(global_object.ajax_url, data, function (resp) {
if ($is_cart && resp.cart && $('.content-area.page-wrapper .woocommerce').length) {
// Clean unwanted classes
resp.cart = resp.cart.replace(/pfc-checks/g, '');
// Create a hidden div to hold the refreshed cart HTML
if ($('.pfc-temp-cart').length === 0) {
$('body').append('<div class="pfc-temp-cart" style="display: none">' + resp.cart + '</div>');
} else {
$('.pfc-temp-cart').html(resp.cart);
}
// Extract the new cart table and totals
const updatedCartTable = $('.pfc-temp-cart').find('.woocommerce-cart-form__contents');
const updatedCartTotals = $('.pfc-temp-cart').find('.cart_totals');
// Replace the current cart table and totals
if (updatedCartTable.length) {
$('.woocommerce-cart-form__contents').html(updatedCartTable.html());
}
if (updatedCartTotals.length) {
$('.cart_totals').html(updatedCartTotals.html());
}
// Hide custom loader if used
$('.pfc-custom-load').hide();
}
// Refresh WooCommerce fragments (like mini-cart, if any)
$(document.body).trigger("wc_fragment_refresh");
$(document.body).trigger("update_checkout");
}, 'json');
—
### ✅ Optional Improvements
– If you're also updating cart notices (e.g. success messages), extract `.woocommerce-notices-wrapper` from `.pfc-temp-cart` and inject it too.
– To support Divi or Elementor themes, you may want to use broader selectors.
– Wrap the whole thing in `$(document).ready()` or in your checkbox trigger callback.
—
Let me know if you want this wrapped into a reusable function or to also work for checkout/cart preview widgets.