WooCommerce Quantity Break Cards adds flexible quantity-based pricing cards to WooCommerce product pages.
Store owners can configure different quantity offers such as Buy 1, Buy 2, Buy 3, Buy 4, etc., and control which offers are available for each individual product.
Features:
- Display quantity-break pricing cards on product pages.
- Enable or disable quantity breaks globally.
- Choose which quantity tiers are available for each product.
- Support Buy 1 only, Buy 1 + Buy 2, Buy 1 + Buy 3, Buy 1 + Buy 2 + Buy 3, or any combination.
- Enable/disable Buy 4 independently.
- Automatically calculate default Buy 4 pricing from each product’s WooCommerce price.
- Set custom total prices for individual quantity tiers.
- Add custom labels and promotional badges such as SAVE 10% or BEST VALUE.
- Disable quantity-break pricing completely on selected products.
- Apply the correct tier price automatically in the WooCommerce cart.
- Support simple and variable products.
- Hide the standard WooCommerce quantity selector when quantity-break cards are active.
- Supports classic WooCommerce product pages and WooCommerce Blocks.
- Products without configured quantity breaks continue using normal WooCommerce pricing.
Example:
Product price: ₹100
- Buy 1 — ₹100
- Buy 2 — ₹190
- Buy 3 — ₹270
- Buy 4 — ₹360
Another product priced at ₹250 automatically uses its own base price when calculating default tiers.
This allows you to configure different quantity offers for every product instead of forcing the same quantity breaks across the entire store.
<?php
/**
* Plugin Name: WooCommerce Quantity Break Cards
* Description: Quantity break pricing cards for WooCommerce with classic and block product-page support.
* Version: 7.1.0
* Author: kishores
* Author URI: https://profiles.wordpress.org/kishores/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'WQBR_Quantity_Break_Cards' ) ) {
class WQBR_Quantity_Break_Cards {
const VERSION = '7.1.0';
const META_TIERS = '_wqbr_tiers';
const OPTION_KEY = 'wqbr_settings';
private $frontend_rendered = false;
public function __construct() {
/* ADMIN */
add_action(
'woocommerce_product_options_pricing',
array( $this, 'admin_fields' )
);
add_action(
'woocommerce_process_product_meta',
array( $this, 'save_fields' )
);
add_action(
'admin_menu',
array( $this, 'admin_menu' )
);
add_action(
'admin_init',
array( $this, 'register_settings' )
);
/* CLASSIC PRODUCT PAGE */
add_action(
'woocommerce_before_add_to_cart_form',
array( $this, 'output' ),
5
);
add_action(
'woocommerce_single_product_summary',
array( $this, 'output_summary' ),
25
);
/* SHORTCODE */
add_shortcode(
'wqbr_quantity_breaks',
array( $this, 'shortcode' )
);
/* BLOCK PRODUCT PAGE */
add_filter(
'render_block',
array( $this, 'render_block_fallback' ),
20,
2
);
/* CART PRICING */
add_action(
'woocommerce_before_calculate_totals',
array( $this, 'apply_pricing' ),
100
);
/* CLASSIC ADD TO CART */
add_filter(
'woocommerce_add_to_cart_quantity',
array( $this, 'filter_add_to_cart_quantity' ),
10,
2
);
/* AJAX */
add_action(
'wp_ajax_wqbr_add_to_cart',
array( $this, 'ajax_add_to_cart' )
);
add_action(
'wp_ajax_nopriv_wqbr_add_to_cart',
array( $this, 'ajax_add_to_cart' )
);
/* FRONTEND */
add_action(
'wp_head',
array( $this, 'css' ),
99
);
add_action(
'wp_footer',
array( $this, 'javascript' ),
99
);
}
/* ============================================================
* SETTINGS
* ============================================================
*/
private function settings() {
$defaults = array(
'enabled' => 'yes',
'use_wc_price' => 'yes',
'hide_quantity' => 'yes',
'redirect' => 'cart',
'use_default_tiers' => 'no',
'clear_cart_on_break' => 'no',
'default_breaks' => 3,
);
$settings = get_option(
self::OPTION_KEY,
array()
);
if ( ! is_array( $settings ) ) {
$settings = array();
}
return wp_parse_args(
$settings,
$defaults
);
}
private function is_enabled() {
$settings = $this->settings();
return 'yes' === $settings['enabled'];
}
/* ============================================================
* ADMIN MENU
* ============================================================
*/
public function admin_menu() {
add_submenu_page(
'woocommerce',
'Quantity Breaks',
'Quantity Breaks',
'manage_woocommerce',
'wqbr-settings',
array(
$this,
'settings_page',
)
);
}
public function register_settings() {
register_setting(
'wqbr_settings_group',
self::OPTION_KEY,
array(
'type' => 'array',
'sanitize_callback' => array(
$this,
'sanitize_settings',
),
'default' => array(),
)
);
}
public function sanitize_settings( $input ) {
$output = array();
$output['enabled'] =
(
isset( $input['enabled'] ) &&
'yes' === $input['enabled']
)
? 'yes'
: 'no';
$output['use_wc_price'] =
(
isset( $input['use_wc_price'] ) &&
'yes' === $input['use_wc_price']
)
? 'yes'
: 'no';
$output['hide_quantity'] =
(
isset( $input['hide_quantity'] ) &&
'yes' === $input['hide_quantity']
)
? 'yes'
: 'no';
/*
* Kept for backwards compatibility.
*
* IMPORTANT:
* This setting no longer creates automatic tiers.
*/
$output['use_default_tiers'] = 'no';
$output['clear_cart_on_break'] =
(
isset( $input['clear_cart_on_break'] ) &&
'yes' === $input['clear_cart_on_break']
)
? 'yes'
: 'no';
$redirect =
isset( $input['redirect'] )
? sanitize_key( $input['redirect'] )
: 'cart';
if (
! in_array(
$redirect,
array(
'stay',
'cart',
'checkout',
),
true
)
) {
$redirect = 'cart';
}
$output['redirect'] = $redirect;
$default_breaks =
isset( $input['default_breaks'] )
? absint( $input['default_breaks'] )
: 3;
$output['default_breaks'] =
max(
1,
min(
10,
$default_breaks
)
);
return $output;
}
public function settings_page() {
if (
! current_user_can(
'manage_woocommerce'
)
) {
return;
}
$settings = $this->settings();
?>
<div class="wrap">
<h1>Quantity Breaks</h1>
<form method="post" action="options.php">
<?php
settings_fields(
'wqbr_settings_group'
);
?>
<table class="form-table">
<tr>
<th scope="row">
Enable Quantity Breaks
</th>
<td>
<label>
<input
type="checkbox"
name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled]"
value="yes"
<?php
checked(
$settings['enabled'],
'yes'
);
?>
>
Enable quantity break cards
</label>
</td>
</tr>
<tr>
<th scope="row">
Use WooCommerce Product Price
</th>
<td>
<label>
<input
type="checkbox"
name="<?php echo esc_attr( self::OPTION_KEY ); ?>[use_wc_price]"
value="yes"
<?php
checked(
$settings['use_wc_price'],
'yes'
);
?>
>
Use WooCommerce's product price.
</label>
<p class="description">
Used when displaying WooCommerce prices.
No automatic quantity tiers are generated.
</p>
</td>
</tr>
<tr>
<th scope="row">
Hide Normal Quantity Field
</th>
<td>
<label>
<input
type="checkbox"
name="<?php echo esc_attr( self::OPTION_KEY ); ?>[hide_quantity]"
value="yes"
<?php
checked(
$settings['hide_quantity'],
'yes'
);
?>
>
Hide the normal WooCommerce quantity selector.
</label>
</td>
</tr>
<tr>
<th scope="row">
Default Quantity Breaks
</th>
<td>
<select
name="<?php echo esc_attr( self::OPTION_KEY ); ?>[default_breaks]"
>
<?php
for (
$i = 1;
$i <= 10;
$i++
) :
?>
<option
value="<?php echo esc_attr( $i ); ?>"
<?php
selected(
(int) $settings['default_breaks'],
$i
);
?>
>
<?php echo esc_html( $i ); ?>
</option>
<?php endfor; ?>
</select>
<p class="description">
Kept for compatibility. Automatic tiers are disabled.
Only manually configured product tiers are used.
</p>
</td>
</tr>
<tr>
<th scope="row">
Default Tiers
</th>
<td>
<p class="description">
Automatic/default tiers are disabled.
A product will show quantity-break cards only when
you manually add at least one valid tier.
</p>
</td>
</tr>
<tr>
<th scope="row">
After Add to Cart
</th>
<td>
<select
name="<?php echo esc_attr( self::OPTION_KEY ); ?>[redirect]"
>
<option
value="stay"
<?php selected( $settings['redirect'], 'stay' ); ?>
>
Stay on Product
</option>
<option
value="cart"
<?php selected( $settings['redirect'], 'cart' ); ?>
>
Go to Cart
</option>
<option
value="checkout"
<?php selected( $settings['redirect'], 'checkout' ); ?>
>
Go to Checkout
</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
Repeated Add to Cart
</th>
<td>
<label>
<input
type="checkbox"
name="<?php echo esc_attr( self::OPTION_KEY ); ?>[clear_cart_on_break]"
value="yes"
<?php
checked(
$settings['clear_cart_on_break'],
'yes'
);
?>
>
Clear the existing cart before adding a new quantity break.
</label>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
/* ============================================================
* BASE PRICE
* ============================================================
*/
private function get_base_price( $product ) {
if ( ! $product instanceof WC_Product ) {
return 0;
}
$current_price =
$product->get_price();
if (
'' !== $current_price &&
is_numeric( $current_price )
) {
$current_price =
(float) $current_price;
if (
$current_price > 0
) {
return $current_price;
}
}
$regular_price =
$product->get_regular_price();
if (
'' !== $regular_price &&
is_numeric( $regular_price )
) {
$regular_price =
(float) $regular_price;
if (
$regular_price > 0
) {
return $regular_price;
}
}
$display_price =
wc_get_price_to_display(
$product,
array(
'qty' => 1,
)
);
if (
is_numeric( $display_price )
) {
$display_price =
(float) $display_price;
if (
$display_price > 0
) {
return $display_price;
}
}
return 0;
}
/* ============================================================
* LEGACY DEFAULTS
*
* Kept so old installations do not break if another piece
* of code references this method.
*
* IMPORTANT:
* This method is NOT called by frontend/cart pricing.
* No automatic tiers are generated.
* ============================================================
*/
private function defaults( $product = false ) {
return array();
}
/* ============================================================
* ADMIN PRODUCT FIELDS
* ============================================================
*/
public function admin_fields() {
global $post;
if ( ! $post ) {
return;
}
/*
* IMPORTANT:
*
* Do NOT generate default tiers here.
*
* If the merchant has not added tiers,
* the list stays empty.
*/
$saved_tiers =
get_post_meta(
$post->ID,
self::META_TIERS,
true
);
if ( is_array( $saved_tiers ) ) {
$tiers = $saved_tiers;
} else {
$tiers = array();
}
?>
<div
class="options_group"
id="wqbr-admin-tiers"
>
<p
style="
padding:12px;
margin:0;
font-size:14px;
font-weight:700;
"
>
Quantity Break Pricing
</p>
<p
style="
padding:0 12px;
color:#666;
"
>
Configure the total price for each quantity.
For example, Quantity 2 Price means the total price
for 2 items.
</p>
<p
style="
padding:0 12px;
color:#2271b1;
font-weight:600;
"
>
If no quantity break is added, this product will use
normal WooCommerce pricing and no quantity-break cards
will be displayed.
</p>
<div
id="wqbr-tier-list"
>
<?php
foreach (
$tiers as $index => $tier
) {
$this->render_admin_tier(
$index,
$tier
);
}
?>
</div>
<p
style="
padding:12px;
"
>
<button
type="button"
class="button"
id="wqbr-add-tier"
>
+ Add Quantity Break
</button>
</p>
<p
style="
padding:0 12px 12px;
color:#666;
"
>
<strong>Important:</strong>
Total Price is the price for the entire selected quantity,
not the per-item price.
</p>
</div>
<script>
jQuery(function($){
var tierIndex =
<?php echo absint( count( $tiers ) ); ?>;
$('#wqbr-add-tier').on(
'click',
function(e){
e.preventDefault();
var index =
tierIndex++;
var qty =
index + 1;
var html = '';
html += '<div class="wqbr-admin-tier"';
html += ' style="margin:12px;padding:15px;background:#f8f8f8;border:1px solid #ddd;border-radius:6px;">';
html += '<p style="font-weight:700;margin-top:0;">';
html += 'Quantity Break ' + qty;
html += '</p>';
html += '<p class="form-field">';
html += '<label>Quantity</label>';
html += '<input type="number" min="1" step="1" ';
html += 'name="<?php echo esc_attr( self::META_TIERS ); ?>[' + index + '][qty]" ';
html += 'value="' + qty + '" />';
html += '</p>';
html += '<p class="form-field">';
html += '<label>Total Price</label>';
html += '<input type="number" min="0.01" step="0.01" ';
html += 'name="<?php echo esc_attr( self::META_TIERS ); ?>[' + index + '][price]" ';
html += 'value="" />';
html += '</p>';
html += '<p class="form-field">';
html += '<label>Label</label>';
html += '<input type="text" ';
html += 'name="<?php echo esc_attr( self::META_TIERS ); ?>[' + index + '][label]" ';
html += 'value="Buy ' + qty + '" />';
html += '</p>';
html += '<p class="form-field">';
html += '<label>Badge</label>';
html += '<input type="text" ';
html += 'name="<?php echo esc_attr( self::META_TIERS ); ?>[' + index + '][badge]" ';
html += 'value="" />';
html += '</p>';
html += '<button type="button" class="button wqbr-remove-tier">';
html += 'Remove';
html += '</button>';
html += '</div>';
$('#wqbr-tier-list').append(
html
);
}
);
$(document).on(
'click',
'.wqbr-remove-tier',
function(e){
e.preventDefault();
$(this)
.closest(
'.wqbr-admin-tier'
)
.remove();
}
);
});
</script>
<?php
}
private function render_admin_tier(
$index,
$tier
) {
$index =
absint(
$index
);
$qty =
isset( $tier['qty'] )
? absint( $tier['qty'] )
: $index + 1;
$price =
isset( $tier['price'] )
? $tier['price']
: '';
$label =
isset( $tier['label'] )
? $tier['label']
: 'Buy ' . $qty;
$badge =
isset( $tier['badge'] )
? $tier['badge']
: '';
?>
<div
class="wqbr-admin-tier"
style="
margin:12px;
padding:15px;
background:#f8f8f8;
border:1px solid #ddd;
border-radius:6px;
"
>
<p
style="
font-weight:700;
margin-top:0;
"
>
Quantity Break <?php echo esc_html( $index + 1 ); ?>
</p>
<?php
woocommerce_wp_text_input(
array(
'id' =>
self::META_TIERS .
'[' . $index . '][qty]',
'name' =>
self::META_TIERS .
'[' . $index . '][qty]',
'label' =>
'Quantity',
'type' =>
'number',
'value' =>
$qty,
'custom_attributes' =>
array(
'min' => '1',
'step' => '1',
),
)
);
woocommerce_wp_text_input(
array(
'id' =>
self::META_TIERS .
'[' . $index . '][price]',
'name' =>
self::META_TIERS .
'[' . $index . '][price]',
'label' =>
'Total Price',
'type' =>
'number',
'value' =>
$price,
'custom_attributes' =>
array(
'min' => '0.01',
'step' => '0.01',
),
)
);
woocommerce_wp_text_input(
array(
'id' =>
self::META_TIERS .
'[' . $index . '][label]',
'name' =>
self::META_TIERS .
'[' . $index . '][label]',
'label' =>
'Label',
'type' =>
'text',
'value' =>
$label,
)
);
woocommerce_wp_text_input(
array(
'id' =>
self::META_TIERS .
'[' . $index . '][badge]',
'name' =>
self::META_TIERS .
'[' . $index . '][badge]',
'label' =>
'Badge',
'type' =>
'text',
'value' =>
$badge,
'description' =>
'Example: SAVE 10% or BEST VALUE',
'desc_tip' =>
true,
)
);
?>
<button
type="button"
class="button wqbr-remove-tier"
>
Remove
</button>
</div>
<?php
}
/* ============================================================
* SAVE PRODUCT FIELDS
* ============================================================
*/
public function save_fields( $product_id ) {
if (
! current_user_can(
'edit_post',
$product_id
)
) {
return;
}
/*
* No tier field submitted.
*
* Delete any old tiers.
*/
if (
! isset(
$_POST[ self::META_TIERS ]
)
) {
delete_post_meta(
$product_id,
self::META_TIERS
);
return;
}
$raw =
wp_unslash(
$_POST[ self::META_TIERS ]
);
$tiers = array();
if (
is_array( $raw )
) {
foreach (
$raw as $tier
) {
if (
! is_array( $tier )
) {
continue;
}
$qty =
isset( $tier['qty'] )
? absint( $tier['qty'] )
: 0;
$price =
isset( $tier['price'] )
? wc_format_decimal(
$tier['price']
)
: '';
/*
* Invalid/empty tier:
*
* - no quantity
* - no price
* - zero price
* - negative price
*
* is completely ignored.
*/
if (
$qty < 1 ||
'' === $price ||
(float) $price <= 0
) {
continue;
}
$label =
isset( $tier['label'] )
? sanitize_text_field(
$tier['label']
)
: '';
$badge =
isset( $tier['badge'] )
? sanitize_text_field(
$tier['badge']
)
: '';
/*
* Empty label gets a sensible fallback.
*/
if ( '' === $label ) {
$label = 'Buy ' . $qty;
}
$tiers[] =
array(
'qty' =>
$qty,
'price' =>
$price,
'label' =>
$label,
'badge' =>
$badge,
);
}
}
/*
* Sort by quantity.
*/
usort(
$tiers,
function(
$a,
$b
) {
return
(int) $a['qty'] -
(int) $b['qty'];
}
);
/*
* Remove duplicate quantities.
*
* If duplicate quantities are submitted,
* the last valid one wins.
*/
$unique = array();
foreach (
$tiers as $tier
) {
$key =
(string)
$tier['qty'];
$unique[ $key ] =
$tier;
}
$tiers =
array_values(
$unique
);
/*
* VERY IMPORTANT:
*
* If the merchant removed every tier,
* delete the meta.
*
* This makes get_tiers() return empty,
* which means:
*
* - no cards
* - no quantity hiding
* - no tier pricing
* - normal WooCommerce price
*/
if (
empty( $tiers )
) {
delete_post_meta(
$product_id,
self::META_TIERS
);
return;
}
update_post_meta(
$product_id,
self::META_TIERS,
$tiers
);
}
/* ============================================================
* GET SAVED TIERS
* ============================================================
*/
private function get_tiers( $product ) {
if (
! $product instanceof WC_Product
) {
return array();
}
$tiers =
get_post_meta(
$product->get_id(),
self::META_TIERS,
true
);
/*
* Variations inherit parent custom tiers.
*/
if (
(
! is_array( $tiers ) ||
empty( $tiers )
) &&
$product->is_type( 'variation' )
) {
$parent_id =
$product->get_parent_id();
if (
$parent_id
) {
$tiers =
get_post_meta(
$parent_id,
self::META_TIERS,
true
);
}
}
/*
* IMPORTANT:
*
* No saved tiers = no quantity breaks.
*/
if (
! is_array( $tiers ) ||
empty( $tiers )
) {
return array();
}
$valid = array();
foreach (
$tiers as $tier
) {
if (
! is_array( $tier )
) {
continue;
}
$qty =
isset( $tier['qty'] )
? absint( $tier['qty'] )
: 0;
$price =
isset( $tier['price'] )
? (float) $tier['price']
: 0;
/*
* Invalid tier = skip.
*/
if (
$qty < 1 ||
$price <= 0
) {
continue;
}
$label =
isset( $tier['label'] )
? sanitize_text_field(
$tier['label']
)
: '';
$badge =
isset( $tier['badge'] )
? sanitize_text_field(
$tier['badge']
)
: '';
if ( '' === $label ) {
$label = 'Buy ' . $qty;
}
$valid[] =
array(
'qty' =>
$qty,
'price' =>
wc_format_decimal(
$price
),
'label' =>
$label,
'badge' =>
$badge,
);
}
/*
* Sort by quantity.
*/
usort(
$valid,
function(
$a,
$b
) {
return
(int) $a['qty'] -
(int) $b['qty'];
}
);
/*
* Remove duplicate quantities.
*/
$unique = array();
foreach (
$valid as $tier
) {
$unique[
(string) $tier['qty']
] = $tier;
}
return array_values(
$unique
);
}
/* ============================================================
* FRONTEND TIERS
* ============================================================
*/
private function get_frontend_tiers( $product ) {
/*
* ONLY explicitly configured tiers.
*
* There is intentionally no fallback to defaults().
*/
return $this->get_tiers(
$product
);
}
/* ============================================================
* OUTPUT SUMMARY
* ============================================================
*/
public function output_summary() {
if (
! is_product()
) {
return;
}
$this->output();
}
/* ============================================================
* OUTPUT
* ============================================================
*/
public function output() {
global $product;
if (
! $product instanceof WC_Product
) {
return;
}
if (
! $this->is_enabled()
) {
return;
}
if (
$this->frontend_rendered
) {
return;
}
$tiers =
$this->get_frontend_tiers(
$product
);
/*
* No tiers = do absolutely nothing.
*/
if (
empty( $tiers )
) {
return;
}
$this->frontend_rendered =
true;
$this->render_cards(
$tiers,
$product
);
}
/* ============================================================
* RENDER CARDS
* ============================================================
*/
private function render_cards(
$tiers,
$product = false
) {
if (
empty( $tiers )
) {
return;
}
$product_id =
$product instanceof WC_Product
? $product->get_id()
: 0;
echo '<div
class="wqbr-wrapper"
data-wqbr="1"
data-product-id="' .
esc_attr(
$product_id
) .
'">';
echo '<div class="wqbr-heading">';
echo esc_html__(
'Choose your quantity',
'wqbr'
);
echo '</div>';
echo '<div class="wqbr-grid">';
$first = true;
foreach (
$tiers as $tier
) {
$qty =
isset( $tier['qty'] )
? absint( $tier['qty'] )
: 0;
$total =
isset( $tier['price'] )
? (float) $tier['price']
: 0;
/*
* Never render invalid cards.
*/
if (
$qty < 1 ||
$total <= 0
) {
continue;
}
$unit =
$total / $qty;
$label =
! empty( $tier['label'] )
? $tier['label']
: 'Buy ' . $qty;
$badge =
! empty( $tier['badge'] )
? $tier['badge']
: '';
echo '<div
class="wqbr-option ' .
(
$first
? 'wqbr-selected'
: ''
) .
'"
data-qty="' .
esc_attr(
$qty
) .
'"
data-total="' .
esc_attr(
wc_format_decimal(
$total
)
) .
'">';
echo '<input
type="radio"
name="wqbr_quantity"
value="' .
esc_attr(
$qty
) .
'" ' .
checked(
$first,
true,
false
) .
'>';
if (
$badge
) {
echo '<div class="wqbr-badge">';
echo esc_html(
$badge
);
echo '</div>';
}
echo '<div class="wqbr-radio"></div>';
echo '<div class="wqbr-details">';
echo '<div class="wqbr-label">';
echo esc_html(
$label
);
echo '</div>';
echo '<div class="wqbr-unit">';
echo wp_kses_post(
wc_price(
$unit
)
);
echo ' ';
echo esc_html__(
'each',
'wqbr'
);
echo '</div>';
echo '</div>';
echo '<div class="wqbr-price">';
echo wp_kses_post(
wc_price(
$total
)
);
echo '</div>';
echo '</div>';
$first = false;
}
echo '</div>';
echo '</div>';
}
/* ============================================================
* SHORTCODE
* ============================================================
*/
public function shortcode() {
if (
! is_product()
) {
return '';
}
global $product;
if (
! $product instanceof WC_Product
) {
return '';
}
if (
! $this->is_enabled()
) {
return '';
}
$tiers =
$this->get_frontend_tiers(
$product
);
if (
empty( $tiers )
) {
return '';
}
ob_start();
$this->render_cards(
$tiers,
$product
);
return ob_get_clean();
}
/* ============================================================
* BLOCK FALLBACK
* ============================================================
*/
public function render_block_fallback(
$block_content,
$block
) {
if (
! is_product()
) {
return $block_content;
}
if (
! $this->is_enabled()
) {
return $block_content;
}
if (
$this->frontend_rendered
) {
return $block_content;
}
if (
empty( $block['blockName'] )
) {
return $block_content;
}
$targets =
array(
'woocommerce/add-to-cart-form',
'woocommerce/add-to-cart-with-options',
'woocommerce/product-button',
'woocommerce/add-to-cart',
);
if (
! in_array(
$block['blockName'],
$targets,
true
)
) {
return $block_content;
}
global $product;
if (
! $product instanceof WC_Product
) {
return $block_content;
}
$tiers =
$this->get_frontend_tiers(
$product
);
/*
* No tier = leave the block completely untouched.
*/
if (
empty( $tiers )
) {
return $block_content;
}
ob_start();
$this->render_cards(
$tiers,
$product
);
$cards =
ob_get_clean();
$this->frontend_rendered =
true;
return
$cards .
$block_content;
}
/* ============================================================
* CLASSIC QUANTITY FILTER
* ============================================================
*/
public function filter_add_to_cart_quantity(
$quantity,
$product_id
) {
if (
! $this->is_enabled()
) {
return $quantity;
}
if (
isset(
$_POST['wqbr_selected_quantity']
)
) {
$selected =
absint(
wp_unslash(
$_POST[
'wqbr_selected_quantity'
]
)
);
if (
$selected > 0
) {
return $selected;
}
}
if (
isset(
$_POST['quantity']
)
) {
$selected =
absint(
wp_unslash(
$_POST['quantity']
)
);
if (
$selected > 0
) {
return $selected;
}
}
return $quantity;
}
/* ============================================================
* AJAX ADD TO CART
* ============================================================
*/
public function ajax_add_to_cart() {
check_ajax_referer(
'wqbr_add_to_cart',
'nonce'
);
if (
! function_exists( 'WC' ) ||
! WC()->cart
) {
wp_send_json_error(
array(
'message' =>
'WooCommerce cart is unavailable.',
)
);
}
$product_id =
isset( $_POST['product_id'] )
? absint( $_POST['product_id'] )
: 0;
$quantity =
isset( $_POST['quantity'] )
? absint( $_POST['quantity'] )
: 0;
$variation_id =
isset( $_POST['variation_id'] )
? absint( $_POST['variation_id'] )
: 0;
if (
$product_id < 1 ||
$quantity < 1
) {
wp_send_json_error(
array(
'message' =>
'Invalid product or quantity.',
)
);
}
$quantity =
max(
1,
$quantity
);
$parent_product =
wc_get_product(
$product_id
);
if (
! $parent_product instanceof WC_Product
) {
wp_send_json_error(
array(
'message' =>
'Product not found.',
)
);
}
$variation = array();
if (
$variation_id > 0
) {
$variation_product =
wc_get_product(
$variation_id
);
if (
! $variation_product instanceof WC_Product_Variation
) {
wp_send_json_error(
array(
'message' =>
'Selected variation is invalid.',
)
);
}
if (
(int)
$variation_product->get_parent_id()
!==
(int)
$product_id
) {
wp_send_json_error(
array(
'message' =>
'Selected variation does not belong to this product.',
)
);
}
$variation =
$variation_product
->get_variation_attributes();
}
$check_product =
$variation_id > 0
? wc_get_product(
$variation_id
)
: $parent_product;
if (
! $check_product instanceof WC_Product
) {
wp_send_json_error(
array(
'message' =>
'Product is unavailable.',
)
);
}
if (
! $check_product->is_purchasable()
) {
wp_send_json_error(
array(
'message' =>
'This product cannot be purchased.',
)
);
}
if (
! $check_product->is_in_stock()
) {
wp_send_json_error(
array(
'message' =>
'This product is out of stock.',
)
);
}
if (
$check_product->managing_stock()
) {
$stock_quantity =
$check_product->get_stock_quantity();
if (
null !== $stock_quantity &&
$quantity > $stock_quantity
) {
wp_send_json_error(
array(
'message' =>
sprintf(
'Only %s item(s) are available.',
$stock_quantity
),
)
);
}
}
$settings =
$this->settings();
if (
'yes' ===
$settings['clear_cart_on_break']
) {
WC()->cart->empty_cart();
}
/*
* Exact quantity goes to WooCommerce.
*/
$cart_item_key =
WC()->cart->add_to_cart(
$product_id,
$quantity,
$variation_id,
$variation
);
if (
! $cart_item_key
) {
wp_send_json_error(
array(
'message' =>
'WooCommerce could not add the selected quantity to the cart.',
)
);
}
$redirect =
wp_get_referer();
if (
'cart' ===
$settings['redirect']
) {
$redirect =
wc_get_cart_url();
} elseif (
'checkout' ===
$settings['redirect']
) {
$redirect =
wc_get_checkout_url();
}
if (
! $redirect
) {
$redirect =
get_permalink(
$product_id
);
}
wp_send_json_success(
array(
'cart_item_key' =>
$cart_item_key,
'quantity' =>
$quantity,
'cart_url' =>
wc_get_cart_url(),
'checkout_url' =>
wc_get_checkout_url(),
'redirect' =>
esc_url_raw(
$redirect
),
'fragments' =>
array(),
'cart_hash' =>
WC()->cart->get_cart_hash(),
)
);
}
/* ============================================================
* APPLY PRICING
* ============================================================
*/
public function apply_pricing( $cart ) {
if (
is_admin() &&
! defined( 'DOING_AJAX' )
) {
return;
}
if (
! $cart instanceof WC_Cart
) {
return;
}
static $running = false;
if (
$running
) {
return;
}
$running = true;
foreach (
$cart->get_cart() as $cart_item
) {
if (
empty(
$cart_item['data']
)
) {
continue;
}
if (
! $cart_item['data']
instanceof WC_Product
) {
continue;
}
$product =
$cart_item['data'];
/*
* ONLY explicitly saved tiers.
*
* No tiers = skip pricing completely.
*/
$tiers =
$this->get_tiers(
$product
);
if (
empty( $tiers )
) {
continue;
}
$quantity =
absint(
$cart_item['quantity']
);
if (
$quantity < 1
) {
continue;
}
$matched = false;
/*
* Find the highest configured tier
* that the cart quantity qualifies for.
*/
foreach (
$tiers as $tier
) {
$tier_qty =
isset( $tier['qty'] )
? absint( $tier['qty'] )
: 0;
$tier_price =
isset( $tier['price'] )
? (float) $tier['price']
: 0;
if (
$tier_qty < 1 ||
$tier_price <= 0
) {
continue;
}
if (
$quantity >= $tier_qty
) {
$matched =
$tier;
}
}
if (
! $matched
) {
continue;
}
$tier_qty =
absint(
$matched['qty']
);
$tier_total =
(float)
$matched['price'];
if (
$tier_qty < 1 ||
$tier_total <= 0
) {
continue;
}
/*
* Tier price is total price for tier quantity.
*
* Example:
*
* Buy 2 = 180
*
* Unit price = 180 / 2 = 90
*
* If cart quantity is 4 and there is no
* Q4 tier, Q2 is used:
*
* 90 x 4 = 360.
*/
$unit_price =
$tier_total /
$tier_qty;
if (
$unit_price <= 0
) {
continue;
}
$product->set_price(
wc_format_decimal(
$unit_price
)
);
}
$running = false;
}
/* ============================================================
* CSS
* ============================================================
*/
public function css() {
if (
! is_product()
) {
return;
}
if (
! $this->is_enabled()
) {
return;
}
$settings =
$this->settings();
?>
<style id="wqbr-style">
.wqbr-wrapper {
display:block !important;
width:100% !important;
clear:both;
margin:18px 0 22px;
box-sizing:border-box;
}
.wqbr-heading {
font-size:17px;
font-weight:700;
margin:0 0 12px;
color:#111;
}
.wqbr-grid {
display:grid !important;
grid-template-columns:
repeat(3,minmax(0,1fr));
gap:10px;
width:100%;
}
.wqbr-option {
position:relative;
display:flex !important;
align-items:center;
min-height:92px;
padding:15px 14px;
border:2px solid #ddd;
border-radius:10px;
background:#fff;
cursor:pointer;
box-sizing:border-box;
transition:all .2s ease;
}
.wqbr-option:hover {
border-color:#111;
}
.wqbr-option.wqbr-selected {
border-color:#111;
background:#f8f8f8;
box-shadow:
0 0 0 1px #111;
}
.wqbr-option input {
display:none !important;
}
.wqbr-radio {
width:19px;
height:19px;
border:2px solid #aaa;
border-radius:50%;
margin-right:9px;
flex:0 0 19px;
box-sizing:border-box;
}
.wqbr-selected .wqbr-radio {
border:5px solid #111;
}
.wqbr-details {
flex:1;
min-width:0;
}
.wqbr-label {
font-size:15px;
font-weight:700;
color:#111;
line-height:1.2;
}
.wqbr-unit {
font-size:12px;
color:#666;
margin-top:4px;
}
.wqbr-price {
font-size:17px;
font-weight:800;
color:#111;
white-space:nowrap;
margin-left:7px;
}
.wqbr-badge {
position:absolute;
top:-10px;
left:50%;
transform:
translateX(-50%);
background:#111;
color:#fff;
font-size:9px;
font-weight:800;
line-height:1;
padding:5px 9px;
border-radius:20px;
white-space:nowrap;
z-index:5;
}
.wqbr-option.wqbr-processing {
opacity:.65;
pointer-events:none;
}
<?php if ( 'yes' === $settings['hide_quantity'] ) : ?>
body.wqbr-active
.woocommerce .quantity,
body.wqbr-active
form.cart .quantity,
body.wqbr-active
.quantity,
body.wqbr-active
.wc-block-components-quantity-selector,
body.wqbr-active
.wc-block-components-product-add-to-cart
.quantity {
display:none !important;
}
<?php endif; ?>
@media(max-width:767px) {
.wqbr-grid {
grid-template-columns:
1fr !important;
}
.wqbr-option {
min-height:76px;
}
}
</style>
<?php
}
/* ============================================================
* JAVASCRIPT
* ============================================================
*/
public function javascript() {
if (
! is_product()
) {
return;
}
if (
! $this->is_enabled()
) {
return;
}
global $product;
if (
! $product instanceof WC_Product
) {
return;
}
/*
* IMPORTANT:
*
* Do not output quantity-break JS when this product
* has no configured tiers.
*/
$tiers =
$this->get_frontend_tiers(
$product
);
if (
empty( $tiers )
) {
return;
}
$settings =
$this->settings();
$nonce =
wp_create_nonce(
'wqbr_add_to_cart'
);
$ajax_url =
admin_url(
'admin-ajax.php'
);
?>
<script id="wqbr-script">
jQuery(function($){
'use strict';
var WQBR = {
productId:
<?php
echo absint(
$product->get_id()
);
?>,
ajaxUrl:
<?php
echo wp_json_encode(
$ajax_url
);
?>,
nonce:
<?php
echo wp_json_encode(
$nonce
);
?>,
redirect:
<?php
echo wp_json_encode(
$settings['redirect']
);
?>,
selectedQuantity:
1,
busy:
false
};
/* =====================================================
* SELECTED QUANTITY
* =====================================================
*/
function getSelectedQuantity() {
var card =
$('.wqbr-option.wqbr-selected')
.first();
if (
!card.length
) {
card =
$('.wqbr-option')
.first();
}
if (
!card.length
) {
return 1;
}
var qty =
parseInt(
card.attr(
'data-qty'
),
10
);
if (
!qty ||
qty < 1
) {
qty = 1;
}
return qty;
}
/* =====================================================
* ACTIVATE
* =====================================================
*/
function activateWQBR() {
if (
!$('.wqbr-wrapper').length
) {
return;
}
$('body')
.addClass(
'wqbr-active'
);
$('.wqbr-wrapper')
.closest(
'.product'
)
.addClass(
'wqbr-active'
);
}
/* =====================================================
* CLASSIC QUANTITY
* =====================================================
*/
function syncClassicQuantity(
qty
) {
$(
'form.cart input.qty, ' +
'.woocommerce-variation-add-to-cart input.qty'
).each(
function(){
$(this)
.val(
qty
)
.attr(
'value',
qty
);
}
);
}
/* =====================================================
* GET VARIATION
* =====================================================
*/
function getVariationId() {
var variation =
$(
'form.variations_form input[name="variation_id"]'
)
.first();
if (
variation.length
) {
var id =
parseInt(
variation.val(),
10
);
if (
id > 0
) {
return id;
}
}
return 0;
}
/* =====================================================
* SELECT CARD
* =====================================================
*/
function selectCard(
card
) {
if (
!card ||
!card.length
) {
return;
}
var qty =
parseInt(
card.attr(
'data-qty'
),
10
);
if (
!qty ||
qty < 1
) {
return;
}
$('.wqbr-option')
.removeClass(
'wqbr-selected'
);
$('.wqbr-option input')
.prop(
'checked',
false
);
card.addClass(
'wqbr-selected'
);
card.find(
'input[type="radio"]'
)
.prop(
'checked',
true
);
WQBR.selectedQuantity =
qty;
syncClassicQuantity(
qty
);
}
/* =====================================================
* CARD CLICK
* =====================================================
*/
$(document).on(
'click',
'.wqbr-option',
function(e){
e.preventDefault();
e.stopPropagation();
selectCard(
$(this)
);
}
);
/* =====================================================
* RADIO
* =====================================================
*/
$(document).on(
'change',
'.wqbr-option input[type="radio"]',
function(e){
e.stopPropagation();
selectCard(
$(this).closest(
'.wqbr-option'
)
);
}
);
/* =====================================================
* ADD TO CART
* =====================================================
*/
function wqbrAddToCart(
quantity,
button
) {
if (
WQBR.busy
) {
return;
}
quantity =
parseInt(
quantity,
10
);
if (
!quantity ||
quantity < 1
) {
quantity = 1;
}
WQBR.busy =
true;
if (
button &&
button.length
) {
button
.addClass(
'wqbr-processing'
)
.attr(
'aria-disabled',
'true'
);
button.data(
'wqbr-original-text',
button.text()
);
button.text(
'Adding...'
);
}
var variationId =
getVariationId();
var selectedQuantity =
quantity;
$.ajax({
url:
WQBR.ajaxUrl,
type:
'POST',
dataType:
'json',
cache:
false,
data: {
action:
'wqbr_add_to_cart',
nonce:
WQBR.nonce,
product_id:
WQBR.productId,
quantity:
selectedQuantity,
variation_id:
variationId
}
})
.done(
function(response){
if (
response &&
response.success &&
response.data
) {
$(document.body)
.trigger(
'added_to_cart',
[
response.data.fragments ||
{},
response.data.cart_hash ||
'',
button
]
);
if (
response.data.redirect
) {
window.location.href =
response.data.redirect;
return;
}
resetButton(
button
);
} else {
var message =
'Unable to add the selected quantity to the cart.';
if (
response &&
response.data &&
response.data.message
) {
message =
response.data.message;
}
alert(
message
);
resetButton(
button
);
}
}
)
.fail(
function(){
alert(
'There was a problem adding the product to the cart.'
);
resetButton(
button
);
}
);
}
/* =====================================================
* RESET BUTTON
* =====================================================
*/
function resetButton(
button
) {
WQBR.busy =
false;
if (
button &&
button.length
) {
button
.removeClass(
'wqbr-processing'
)
.removeAttr(
'aria-disabled'
);
var original =
button.data(
'wqbr-original-text'
);
if (
original
) {
button.text(
original
);
}
}
}
/* =====================================================
* ADD TO CART BUTTON
* =====================================================
*/
$(document).on(
'click',
[
'form.cart button.single_add_to_cart_button',
'form.cart .single_add_to_cart_button',
'.wc-block-components-product-button button',
'.wc-block-components-product-button a',
'.wp-block-woocommerce-product-button button',
'.wp-block-woocommerce-product-button a',
'.wc-block-components-add-to-cart-button',
'button[name="add-to-cart"]',
'a.single_add_to_cart_button'
].join(','),
function(e){
if (
!$('.wqbr-wrapper').length
) {
return;
}
var button =
$(this);
var variationForm =
$('form.variations_form');
if (
variationForm.length &&
!getVariationId()
) {
return;
}
var quantity =
getSelectedQuantity();
WQBR.selectedQuantity =
quantity;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
syncClassicQuantity(
quantity
);
wqbrAddToCart(
quantity,
button
);
return false;
}
);
/* =====================================================
* VARIABLE PRODUCT
*
* IMPORTANT:
*
* Cards are NOT automatically recalculated from
* variation price anymore.
*
* The configured tier TOTAL prices remain exactly
* what the merchant entered.
* =====================================================
*/
$('form.variations_form')
.on(
'found_variation',
function(
event,
variation
){
if (
!variation
) {
return;
}
/*
* Configured tier prices remain unchanged.
*
* This prevents a variation price from
* accidentally creating automatic tiers.
*/
}
);
/* =====================================================
* INITIALIZE
* =====================================================
*/
function initialize() {
activateWQBR();
var selected =
$('.wqbr-option.wqbr-selected')
.first();
if (
!selected.length
) {
selected =
$('.wqbr-option')
.first();
}
if (
selected.length
) {
selectCard(
selected
);
}
WQBR.selectedQuantity =
getSelectedQuantity();
syncClassicQuantity(
WQBR.selectedQuantity
);
}
initialize();
setTimeout(
initialize,
300
);
setTimeout(
initialize,
1000
);
/* =====================================================
* BLOCK THEME OBSERVER
* =====================================================
*/
if (
window.MutationObserver
) {
var observer =
new MutationObserver(
function(){
activateWQBR();
}
);
observer.observe(
document.body,
{
childList:
true,
subtree:
true
}
);
}
});
</script>
<?php
}
}
/* ================================================================
* INITIALIZE
* ================================================================
*/
add_action(
'plugins_loaded',
function() {
if (
class_exists(
'WooCommerce'
)
) {
new WQBR_Quantity_Break_Cards();
}
}
);
}