diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1dd95b0..a568328 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,96 @@ All notable changes to WooCommerce Tier and Package Prices will be documented in
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.2.0] - 2025-12-29
+
+### Added - Variable Product Support
+
+**Major Feature**: Complete support for WooCommerce variable products with variation-level pricing
+
+- Variable products can now have tier and package pricing configured independently for each variation
+- Admin UI: Each variation displays tier/package pricing fields in the variation edit panel
+- Frontend: Pricing tables load dynamically via AJAX when customer selects a variation
+- Cart: Variation-specific pricing correctly applied during checkout
+- Quantity restrictions work per-variation (not just per-product)
+- Catalog buttons: "View Options" appears for variable products with restricted variations
+
+### Changed
+
+- **Admin Templates**: Converted tier/package row templates from `
` to `
` table structure for better layout
+- **Admin UI**: Simple product pricing fields now use table layout for consistency with variations
+- **Frontend Display**: Variable products show placeholder container; pricing appears on variation selection
+- **Cart Logic**: All cart methods now use "effective ID" pattern (variation ID when present, product ID otherwise)
+- **Template System**: Added `field_prefix` parameter support to admin templates for variation field naming
+
+### Technical Details
+
+#### Backend Changes
+
+- **class-wc-tpp-cart.php**: Added variation ID resolution throughout; updated all meta lookups to use effective ID
+- **class-wc-tpp-frontend.php**:
+ - Updated `get_tier_price()` and `get_package_price()` to accept `variation_id` parameter
+ - Added AJAX endpoint `ajax_get_variation_pricing()` for fetching variation pricing data
+ - Updated `display_pricing_table()` to detect variable products and show placeholder
+ - Fixed `modify_catalog_add_to_cart_button()` to check variations for restrictions
+- **class-wc-tpp-product-meta.php**:
+ - Added hooks: `woocommerce_variation_options_pricing`, `woocommerce_save_product_variation`
+ - New method: `add_variation_pricing_fields()` - renders pricing UI in variation panels
+ - New method: `save_variation_pricing_fields()` - saves variation-specific pricing data
+ - New methods: `render_variation_tier_row()`, `render_variation_package_row()` - variation rendering helpers
+
+#### Frontend Changes
+
+- **frontend.js**:
+ - Added variation selector integration listening to `found_variation` and `reset_data` events
+ - Implemented AJAX fetching of variation pricing when variation selected
+ - Dynamic quantity restriction handling per-variation
+ - Re-initialization of event handlers for dynamically loaded pricing tables
+- **admin.js**:
+ - Separated simple product and variation handlers
+ - Variation-specific add/remove tier/package row management
+ - Context-aware template selection using variation loop index
+
+#### Template Changes
+
+- **tier-row.twig**: Added `field_prefix` variable for variation field naming; changed to `
` structure
+- **package-row.twig**: Added `field_prefix` variable for variation field naming; changed to `
` structure
+
+#### Data Storage
+
+- Meta keys remain the same: `_wc_tpp_tiers`, `_wc_tpp_packages`, `_wc_tpp_restrict_to_packages`
+- Simple products: Stored on product post meta
+- Variations: Stored on variation post meta (independent per-variation)
+
+### Backward Compatibility
+
+- **100% backward compatible** - No breaking changes
+- Simple products continue working exactly as before
+- Existing tier/package data unaffected
+- No database migrations required
+- Templates remain compatible (field_prefix optional)
+
+### Migration Notes
+
+- Existing installations can upgrade seamlessly
+- Variable products simply gain new functionality
+- No action required for existing simple product configurations
+
+### Performance Considerations
+
+- AJAX requests only made when variation selected (not on page load)
+- Pricing data fetched per-variation (not all variations at once)
+- Nonce verification on all AJAX requests for security
+- Template rendering server-side for SEO/performance
+
+### Testing Performed
+
+- Simple products: All existing functionality verified
+- Variable products: Tier pricing, package pricing, restrictions tested per-variation
+- Mixed carts: Simple + variable products working correctly
+- WooCommerce Blocks: Cart block, mini-cart block, checkout block compatibility verified
+- Admin UI: Add/remove rows working for both simple products and variations
+- Quantity restrictions: Enforced correctly per-variation in cart and checkout
+
## [1.1.22] - 2025-12-23
### Changed
diff --git a/CLAUDE.md b/CLAUDE.md
index 41a30d4..2f900a3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,7 +1,7 @@
# WooCommerce Tier and Package Prices - AI Context Document
-**Last Updated:** 2025-12-23
-**Current Version:** 1.1.22
+**Last Updated:** 2025-12-29
+**Current Version:** 1.2.0
**Author:** Marco Graetsch
**Project Status:** Production-ready WordPress plugin
@@ -549,11 +549,12 @@ Based on v1.1.22 release experience, here's the complete workflow:
The is a hierarchical list for upcoming features and can be considered as a
Roadmap for the upcoming development.
-#### Version 1.1.x
+#### Version 1.1.x (Completed)
1. ~~Add translations for `de_CH`, `de_DE_informal`, `fr_CH`, `it_CH`~~ ✅ **COMPLETED in v1.1.21**
2. ~~The double-install bug is back again. A new version of the plugin is installed as new plugin instead of updating the previous version.~~ ✅ **DOCUMENTED in v1.1.22** - Added workaround to CHANGELOG. Root cause: No automatic update mechanism (requires WordPress.org repository or custom update server).
3. ~~Make the label fields in the backend for tierprices and package-prices twice as long as it is.~~ ✅ **COMPLETED in v1.1.22**
+4. ~~Make the plugin work with variable products~~ ✅ **COMPLETED in v1.2.0** - Full variation-level pricing support with independent configuration per variation, AJAX-based frontend display, and complete WooCommerce Blocks compatibility.
#### Version 1.2.x
diff --git a/README.md b/README.md
index 25ec75e..b2ce554 100644
--- a/README.md
+++ b/README.md
@@ -171,7 +171,7 @@ A: The price will automatically recalculate based on the new quantity.
A: Yes, each product can have its own tier and package pricing configuration.
**Q: Does this work with variable products?**
-A: Currently, this plugin is designed for simple products. Variable product support may be added in future versions.
+A: Yes! Since version 1.2.0, the plugin fully supports variable products. Each variation can have its own independent tier and package pricing configuration.
## Support
@@ -183,9 +183,22 @@ This plugin is licensed under the GPL v2 or later.
## Changelog
-### Version 1.1.20 - 2025-12-23
+### Version 1.2.0 - 2025-12-29
-**Current Release** - Latest stable version with full WooCommerce Blocks support
+__Current Release__ - Variable Product Support
+
+- __New__: Full support for WooCommerce variable products with variation-level pricing
+- __New__: Each variation can have independent tier and package pricing configuration
+- __New__: AJAX-powered dynamic pricing table display when variations are selected
+- __Changed__: Admin templates converted to table structure for better layout
+- __Fixed__: Quantity restrictions now work correctly per-variation
+- 100% backward compatible - no breaking changes
+
+See [CHANGELOG.md](CHANGELOG.md) for complete details.
+
+### Version 1.1.22 - 2025-12-23
+
+- Increased width of label input fields in admin interface
#### Fixed
- **CRITICAL:** WooCommerce Blocks fatal error in mini-cart and cart blocks
diff --git a/assets/js/admin.js b/assets/js/admin.js
index ba7fab1..a4c3c82 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -6,18 +6,72 @@
'use strict';
$(document).ready(function() {
- let tierIndex = $('.wc-tpp-tier-row').length;
- let packageIndex = $('.wc-tpp-package-row').length;
+ // Initialize indexes for simple products
+ let tierIndex = $('.wc-tpp-tier-pricing .wc-tpp-tier-row').length;
+ let packageIndex = $('.wc-tpp-package-pricing .wc-tpp-package-row').length;
- // Add tier
- $('.wc-tpp-add-tier').on('click', function(e) {
+ // ========================================
+ // Simple Product Handlers
+ // ========================================
+
+ // Add tier (simple products)
+ $('.wc-tpp-tier-pricing .wc-tpp-add-tier').on('click', function(e) {
e.preventDefault();
const template = $('#wc-tpp-tier-row-template').html();
const newRow = template.replace(/\{\{INDEX\}\}/g, tierIndex);
- $('.wc-tpp-tiers-container').append(newRow);
+ $('.wc-tpp-tier-pricing .wc-tpp-tiers-container').append(newRow);
tierIndex++;
});
+ // Add package (simple products)
+ $('.wc-tpp-package-pricing .wc-tpp-add-package').on('click', function(e) {
+ e.preventDefault();
+ const template = $('#wc-tpp-package-row-template').html();
+ const newRow = template.replace(/\{\{INDEX\}\}/g, packageIndex);
+ $('.wc-tpp-package-pricing .wc-tpp-packages-container').append(newRow);
+ packageIndex++;
+ });
+
+ // ========================================
+ // Variable Product Variation Handlers
+ // ========================================
+
+ // Add tier (variations)
+ $(document).on('click', '.wc-tpp-variation-pricing .wc-tpp-add-tier', function(e) {
+ e.preventDefault();
+ const $button = $(this);
+ const loop = $button.data('loop');
+ const $container = $button.closest('.wc-tpp-variation-pricing');
+ const $tbody = $container.find('.wc-tpp-variation-tiers .wc-tpp-tiers-container');
+ const template = $('#wc-tpp-variation-tier-row-template-' + loop).html();
+
+ // Count existing rows to get next index
+ const currentIndex = $tbody.find('tr').length;
+ const newRow = template.replace(/\{\{INDEX\}\}/g, currentIndex);
+
+ $tbody.append(newRow);
+ });
+
+ // Add package (variations)
+ $(document).on('click', '.wc-tpp-variation-pricing .wc-tpp-add-package', function(e) {
+ e.preventDefault();
+ const $button = $(this);
+ const loop = $button.data('loop');
+ const $container = $button.closest('.wc-tpp-variation-pricing');
+ const $tbody = $container.find('.wc-tpp-variation-packages .wc-tpp-packages-container');
+ const template = $('#wc-tpp-variation-package-row-template-' + loop).html();
+
+ // Count existing rows to get next index
+ const currentIndex = $tbody.find('tr').length;
+ const newRow = template.replace(/\{\{INDEX\}\}/g, currentIndex);
+
+ $tbody.append(newRow);
+ });
+
+ // ========================================
+ // Common Handlers (both simple and variations)
+ // ========================================
+
// Remove tier
$(document).on('click', '.wc-tpp-remove-tier', function(e) {
e.preventDefault();
@@ -26,15 +80,6 @@
}
});
- // Add package
- $('.wc-tpp-add-package').on('click', function(e) {
- e.preventDefault();
- const template = $('#wc-tpp-package-row-template').html();
- const newRow = template.replace(/\{\{INDEX\}\}/g, packageIndex);
- $('.wc-tpp-packages-container').append(newRow);
- packageIndex++;
- });
-
// Remove package
$(document).on('click', '.wc-tpp-remove-package', function(e) {
e.preventDefault();
@@ -43,7 +88,7 @@
}
});
- // Validate inputs
+ // Validate quantity inputs
$(document).on('input', 'input[name*="[min_qty]"], input[name*="[qty]"]', function() {
const value = parseInt($(this).val());
if (value < 1) {
@@ -51,6 +96,7 @@
}
});
+ // Validate price inputs
$(document).on('input', 'input[name*="[price]"]', function() {
const value = parseFloat($(this).val());
if (value < 0) {
diff --git a/assets/js/frontend.js b/assets/js/frontend.js
index 8e36491..6a8614a 100644
--- a/assets/js/frontend.js
+++ b/assets/js/frontend.js
@@ -244,6 +244,114 @@
if ($quantityInput.length > 0 && $addToCartButton.length > 0) {
updateAddToCartButton();
}
+
+ // ========================================
+ // Variable Product Support
+ // ========================================
+
+ const $variationsForm = $('.variations_form');
+ const $pricingTableContainer = $('.wc-tpp-pricing-table-container');
+
+ if ($variationsForm.length && $pricingTableContainer.length) {
+ // Handle variation selection
+ $variationsForm.on('found_variation', function(event, variation) {
+ if (!variation.variation_id) {
+ return;
+ }
+
+ // Show loading state
+ $pricingTableContainer.html('
Loading pricing...
').show();
+
+ // Fetch variation pricing via AJAX
+ $.ajax({
+ url: wcTppData.ajax_url,
+ type: 'POST',
+ data: {
+ action: 'wc_tpp_get_variation_pricing',
+ nonce: wcTppData.nonce,
+ variation_id: variation.variation_id
+ },
+ success: function(response) {
+ if (response.success && response.data.has_pricing) {
+ // Display the pricing table HTML
+ $pricingTableContainer.html(response.data.html).show();
+
+ // Re-initialize event handlers for the new content
+ initializePricingHandlers();
+
+ // Handle quantity restrictions
+ if (response.data.restrict_to_packages) {
+ $('input.qty').hide().closest('.quantity').hide();
+ $('').appendTo('head');
+ } else {
+ $('input.qty').show().closest('.quantity').show();
+ $('style:contains(".quantity { display: none")').remove();
+ }
+ } else {
+ // No pricing for this variation
+ $pricingTableContainer.html('').hide();
+ $('input.qty').show().closest('.quantity').show();
+ $('style:contains(".quantity { display: none")').remove();
+ }
+ },
+ error: function() {
+ $pricingTableContainer.html('').hide();
+ }
+ });
+ });
+
+ // Handle variation reset
+ $variationsForm.on('reset_data', function() {
+ $pricingTableContainer.html('').hide();
+ $('input.qty').show().closest('.quantity').show();
+ $('style:contains(".quantity { display: none")').remove();
+ });
+
+ // Initialize pricing handlers for dynamically loaded content
+ function initializePricingHandlers() {
+ // Re-attach package selection handlers
+ $('.wc-tpp-select-package').off('click').on('click', function(e) {
+ e.preventDefault();
+ const $package = $(this).closest('.wc-tpp-package');
+ const qty = parseInt($package.data('qty'));
+ const $qtyInput = $('input.qty');
+
+ if ($qtyInput.length === 0 || $qtyInput.is(':hidden')) {
+ // Create hidden input for restricted products
+ if ($('.qty-hidden-input').length === 0) {
+ $('.single_add_to_cart_button').before('');
+ }
+ $('.qty-hidden-input').val(qty);
+ } else {
+ $qtyInput.val(qty).trigger('change');
+ }
+
+ // Highlight selected package
+ $('.wc-tpp-package').removeClass('wc-tpp-selected');
+ $package.addClass('wc-tpp-selected');
+
+ // Scroll to add to cart button
+ $('html, body').animate({
+ scrollTop: $('.single_add_to_cart_button').offset().top - 100
+ }, 500);
+ });
+
+ // Re-attach tier row click handlers
+ $('.wc-tpp-tier-pricing-table tbody tr').off('click').on('click', function() {
+ const minQty = parseInt($(this).data('min-qty'));
+ const $qtyInput = $('input.qty');
+
+ if ($qtyInput.length > 0 && $qtyInput.is(':visible')) {
+ $qtyInput.val(minQty).trigger('change');
+
+ // Scroll to quantity input
+ $('html, body').animate({
+ scrollTop: $qtyInput.offset().top - 100
+ }, 300);
+ }
+ });
+ }
+ }
});
})(jQuery);
diff --git a/composer.json b/composer.json
index ace9eb6..7ec7bf1 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,7 @@
{
"name": "magdev/wc-tier-package-prices",
"description": "WooCommerce plugin for tier pricing and package prices with Twig templates",
- "version": "1.1.22",
+ "version": "1.2.0",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
diff --git a/includes/class-wc-tpp-cart.php b/includes/class-wc-tpp-cart.php
index 96487fc..2ca95fc 100644
--- a/includes/class-wc-tpp-cart.php
+++ b/includes/class-wc-tpp-cart.php
@@ -40,6 +40,8 @@ if (!class_exists('WC_TPP_Cart')) {
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
$product_id = $cart_item['product_id'];
+ $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
$quantity = $cart_item['quantity'];
$product = $cart_item['data'];
@@ -48,10 +50,10 @@ if (!class_exists('WC_TPP_Cart')) {
continue;
}
- // Check for exact package match first
+ // Check for exact package match first (use effective ID for variations)
$package_price = null;
if (get_option('wc_tpp_enable_package_pricing') === 'yes') {
- $package_price = WC_TPP_Frontend::get_package_price($product_id, $quantity);
+ $package_price = WC_TPP_Frontend::get_package_price($effective_id, $quantity, $variation_id);
}
if ($package_price !== null) {
@@ -62,9 +64,9 @@ if (!class_exists('WC_TPP_Cart')) {
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'package';
WC()->cart->cart_contents[$cart_item_key]['wc_tpp_total_price'] = $package_price;
} else {
- // Apply tier pricing if no package match
+ // Apply tier pricing if no package match (use effective ID for variations)
if (get_option('wc_tpp_enable_tier_pricing') === 'yes') {
- $tier_price = WC_TPP_Frontend::get_tier_price($product_id, $quantity);
+ $tier_price = WC_TPP_Frontend::get_tier_price($effective_id, $quantity, $variation_id);
if ($tier_price !== null) {
$product->set_price($tier_price);
// Store pricing information in cart item for display
@@ -99,16 +101,20 @@ if (!class_exists('WC_TPP_Cart')) {
}
public function validate_package_quantity($passed, $product_id, $quantity) {
- // Check if restriction is enabled globally or for this product
+ // Check for variation ID in request (for variable products)
+ $variation_id = isset($_REQUEST['variation_id']) ? absint($_REQUEST['variation_id']) : 0;
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
+
+ // Check if restriction is enabled globally or for this product/variation
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
- $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
+ $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
if (!$global_restrict && !$product_restrict) {
return $passed;
}
- // Get packages for this product
- $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
+ // Get packages for this product/variation
+ $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
if (empty($packages) || !is_array($packages)) {
return $passed;
@@ -147,18 +153,20 @@ if (!class_exists('WC_TPP_Cart')) {
public function maybe_hide_cart_quantity_input($product_quantity, $cart_item_key, $cart_item) {
$product_id = $cart_item['product_id'];
+ $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
- // Check if restriction is enabled globally or for this product
+ // Check if restriction is enabled globally or for this product/variation
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
- $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
+ $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
- // Get packages for this product
- $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
+ // Get packages for this product/variation
+ $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
// If restriction is enabled and packages exist, show quantity as text only
if (($global_restrict || $product_restrict) && !empty($packages)) {
return sprintf('%s',
- $product_id,
+ $effective_id,
$cart_item['quantity']
);
}
@@ -168,18 +176,20 @@ if (!class_exists('WC_TPP_Cart')) {
public function maybe_hide_mini_cart_quantity_input($product_quantity, $cart_item, $cart_item_key) {
$product_id = $cart_item['product_id'];
+ $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
- // Check if restriction is enabled globally or for this product
+ // Check if restriction is enabled globally or for this product/variation
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
- $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
+ $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
- // Get packages for this product
- $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
+ // Get packages for this product/variation
+ $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
// If restriction is enabled and packages exist, show quantity as text only
if (($global_restrict || $product_restrict) && !empty($packages)) {
return sprintf('%s ×',
- $product_id,
+ $effective_id,
$cart_item['quantity']
);
}
@@ -196,12 +206,15 @@ if (!class_exists('WC_TPP_Cart')) {
$restricted_products = array();
foreach (WC()->cart->get_cart() as $cart_item) {
$product_id = $cart_item['product_id'];
+ $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0;
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
+
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
- $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
- $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
+ $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
+ $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
if (($global_restrict || $product_restrict) && !empty($packages)) {
- $restricted_products[] = $product_id;
+ $restricted_products[] = $effective_id;
}
}
@@ -226,7 +239,7 @@ if (!class_exists('WC_TPP_Cart')) {
* Make quantity non-editable for restricted products in WooCommerce blocks
*
* @param bool $editable Whether the quantity is editable
- * @param WC_Product $product Product object
+ * @param WC_Product $product Product object (can be variation)
* @return bool
*/
public function block_quantity_editable($editable, $product) {
@@ -241,9 +254,12 @@ if (!class_exists('WC_TPP_Cart')) {
return $editable;
}
+ // For variations, use the variation ID directly (get_id() returns variation ID for WC_Product_Variation)
+ $effective_id = $product_id;
+
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
- $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
- $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
+ $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
+ $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
// If restriction is enabled and packages exist, make quantity non-editable
if (($global_restrict || $product_restrict) && !empty($packages)) {
diff --git a/includes/class-wc-tpp-frontend.php b/includes/class-wc-tpp-frontend.php
index ec22a99..5dc6cad 100644
--- a/includes/class-wc-tpp-frontend.php
+++ b/includes/class-wc-tpp-frontend.php
@@ -19,6 +19,10 @@ if (!class_exists('WC_TPP_Frontend')) {
// Modify catalog add to cart button for restricted products
add_filter('woocommerce_loop_add_to_cart_link', array($this, 'modify_catalog_add_to_cart_button'), 10, 2);
+
+ // AJAX endpoints for variation pricing
+ add_action('wp_ajax_wc_tpp_get_variation_pricing', array($this, 'ajax_get_variation_pricing'));
+ add_action('wp_ajax_nopriv_wc_tpp_get_variation_pricing', array($this, 'ajax_get_variation_pricing'));
}
public function enqueue_scripts() {
@@ -31,8 +35,10 @@ if (!class_exists('WC_TPP_Frontend')) {
if (is_product()) {
wp_enqueue_script('wc-tpp-frontend', WC_TPP_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), WC_TPP_VERSION, true);
- // Localize script with currency settings
+ // Localize script with currency settings and AJAX data
wp_localize_script('wc-tpp-frontend', 'wcTppData', array(
+ 'ajax_url' => admin_url('admin-ajax.php'),
+ 'nonce' => wp_create_nonce('wc_tpp_variation_pricing'),
'currency_symbol' => esc_js(get_woocommerce_currency_symbol()),
'currency_position' => esc_js(get_option('woocommerce_currency_pos', 'left')),
'price_decimals' => absint(wc_get_price_decimals()),
@@ -67,6 +73,11 @@ if (!class_exists('WC_TPP_Frontend')) {
return;
}
+ // For variable products, quantity hiding is handled per-variation via JS
+ if ($product->is_type('variable')) {
+ return;
+ }
+
$product_id = $product->get_id();
$global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
$product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
@@ -85,6 +96,13 @@ if (!class_exists('WC_TPP_Frontend')) {
return;
}
+ // For variable products, show a placeholder that will be populated by JS when variation is selected
+ if ($product->is_type('variable')) {
+ echo '';
+ return;
+ }
+
+ // For simple products, display pricing table directly
$product_id = $product->get_id();
$tiers = get_post_meta($product_id, '_wc_tpp_tiers', true);
$packages = get_post_meta($product_id, '_wc_tpp_packages', true);
@@ -103,8 +121,17 @@ if (!class_exists('WC_TPP_Frontend')) {
));
}
- public static function get_tier_price($product_id, $quantity) {
- $tiers = get_post_meta($product_id, '_wc_tpp_tiers', true);
+ /**
+ * Get tier price for a product or variation
+ *
+ * @param int $product_id Product ID (parent for simple, parent for variable)
+ * @param int $quantity Quantity
+ * @param int $variation_id Variation ID (0 for simple products)
+ * @return float|null Tier price or null if not applicable
+ */
+ public static function get_tier_price($product_id, $quantity, $variation_id = 0) {
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
+ $tiers = get_post_meta($effective_id, '_wc_tpp_tiers', true);
if (empty($tiers) || !is_array($tiers)) {
return null;
@@ -120,8 +147,17 @@ if (!class_exists('WC_TPP_Frontend')) {
return $applicable_price;
}
- public static function get_package_price($product_id, $quantity) {
- $packages = get_post_meta($product_id, '_wc_tpp_packages', true);
+ /**
+ * Get package price for a product or variation
+ *
+ * @param int $product_id Product ID (parent for simple, parent for variable)
+ * @param int $quantity Quantity
+ * @param int $variation_id Variation ID (0 for simple products)
+ * @return float|null Package price or null if not applicable
+ */
+ public static function get_package_price($product_id, $quantity, $variation_id = 0) {
+ $effective_id = $variation_id > 0 ? $variation_id : $product_id;
+ $packages = get_post_meta($effective_id, '_wc_tpp_packages', true);
if (empty($packages) || !is_array($packages)) {
return null;
@@ -164,8 +200,24 @@ if (!class_exists('WC_TPP_Frontend')) {
$product_id = $product->get_id();
- // Check if product has quantity restrictions
- if (!self::has_quantity_restriction($product_id)) {
+ // For variable products, check if ANY variation has restrictions
+ // For simple products, check the product itself
+ $has_restriction = false;
+
+ if ($product->is_type('variable')) {
+ // Check if any variation has package restrictions
+ $variations = $product->get_available_variations();
+ foreach ($variations as $variation_data) {
+ if (self::has_quantity_restriction($variation_data['variation_id'])) {
+ $has_restriction = true;
+ break;
+ }
+ }
+ } else {
+ $has_restriction = self::has_quantity_restriction($product_id);
+ }
+
+ if (!$has_restriction) {
return $html;
}
@@ -173,15 +225,72 @@ if (!class_exists('WC_TPP_Frontend')) {
$product_url = esc_url($product->get_permalink());
$button_text = esc_html__('View Options', 'wc-tier-package-prices');
+ // Use correct product type class
+ $product_type_class = $product->is_type('variable') ? 'product_type_variable' : 'product_type_simple';
+
$new_html = sprintf(
- '%s',
+ '%s',
$product_url,
+ esc_attr($product_type_class),
esc_attr(sprintf(__('View options for %s', 'wc-tier-package-prices'), $product->get_name())),
$button_text
);
return $new_html;
}
+
+ /**
+ * AJAX handler to get variation pricing data
+ */
+ public function ajax_get_variation_pricing() {
+ // Verify nonce
+ check_ajax_referer('wc_tpp_variation_pricing', 'nonce');
+
+ $variation_id = isset($_POST['variation_id']) ? absint($_POST['variation_id']) : 0;
+
+ if (!$variation_id) {
+ wp_send_json_error(array('message' => __('Invalid variation ID', 'wc-tier-package-prices')));
+ }
+
+ // Get variation data
+ $variation = wc_get_product($variation_id);
+
+ if (!$variation || !$variation->is_type('variation')) {
+ wp_send_json_error(array('message' => __('Variation not found', 'wc-tier-package-prices')));
+ }
+
+ // Get tier and package pricing
+ $tiers = get_post_meta($variation_id, '_wc_tpp_tiers', true);
+ $packages = get_post_meta($variation_id, '_wc_tpp_packages', true);
+ $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes';
+ $product_restrict = get_post_meta($variation_id, '_wc_tpp_restrict_to_packages', true) === 'yes';
+
+ if (empty($tiers) && empty($packages)) {
+ // No pricing data for this variation
+ wp_send_json_success(array(
+ 'has_pricing' => false,
+ 'html' => ''
+ ));
+ }
+
+ // Render the pricing table HTML
+ ob_start();
+ WC_TPP_Template_Loader::get_instance()->display('frontend/pricing-table.twig', array(
+ 'product' => $variation,
+ 'tiers' => $tiers,
+ 'packages' => $packages,
+ 'restrict_to_packages' => $global_restrict || $product_restrict
+ ));
+ $html = ob_get_clean();
+
+ wp_send_json_success(array(
+ 'has_pricing' => true,
+ 'html' => $html,
+ 'tiers' => $tiers ? $tiers : array(),
+ 'packages' => $packages ? $packages : array(),
+ 'restrict_to_packages' => $global_restrict || $product_restrict
+ ));
+ }
}
new WC_TPP_Frontend();
diff --git a/includes/class-wc-tpp-product-meta.php b/includes/class-wc-tpp-product-meta.php
index eede0a8..11ea6a4 100644
--- a/includes/class-wc-tpp-product-meta.php
+++ b/includes/class-wc-tpp-product-meta.php
@@ -11,9 +11,14 @@ if (!class_exists('WC_TPP_Product_Meta')) {
class WC_TPP_Product_Meta {
public function __construct() {
+ // Simple product hooks
add_action('woocommerce_product_options_pricing', array($this, 'add_tier_pricing_fields'));
add_action('woocommerce_product_options_pricing', array($this, 'add_package_pricing_fields'));
add_action('woocommerce_process_product_meta', array($this, 'save_tier_package_fields'));
+
+ // Variable product variation hooks
+ add_action('woocommerce_variation_options_pricing', array($this, 'add_variation_pricing_fields'), 10, 3);
+ add_action('woocommerce_save_product_variation', array($this, 'save_variation_pricing_fields'), 10, 2);
}
public function add_tier_pricing_fields() {
@@ -25,18 +30,28 @@ if (!class_exists('WC_TPP_Product_Meta')) {
-