Skip to main content
Login Join
Snippet · PHP

WooCommerce Quantity Break Cards

Shared by Kishore Sahoo (Looking for Job) · August 19, 2026

10 views
Back to Snippets

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:

Example:

Product price: ₹100

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();
			}
		}
	);
}
Know a different way to do this? Add your approach as a variation so folks can compare them side by side.
Submit a variation

0 comments