Fix licenses not showing in admin order form for variable products (v0.5.13)

- Fix OrderLicenseController to use isLicensedProduct() for consistent product type detection
- Fixed expected licenses calculation for variable product orders
- Fixed manual license generation from admin order page for variable products
- Remove debug logging from all source files (PHP and JavaScript)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 15:45:32 +01:00
parent 142500cab0
commit d29697ac62
9 changed files with 191 additions and 34 deletions

View File

@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.5.13] - 2026-01-27
### Fixed
- **CRITICAL:** Fixed licenses not showing in admin order form for licensed-variable products
- `OrderLicenseController` now uses `LicenseManager::isLicensedProduct()` for consistent product type detection
- Fixed expected licenses calculation for variable product orders
- Fixed manual license generation from admin order page for variable products
### Changed
- Removed debug logging from all source files (PHP and JavaScript)
- Cleaned up checkout blocks integration, Store API extension, and checkout controller
## [0.5.12] - 2026-01-27 ## [0.5.12] - 2026-01-27
### Fixed ### Fixed

View File

@@ -18,12 +18,25 @@
} }
const { getSetting } = wc.wcSettings; const { getSetting } = wc.wcSettings;
const { createElement, useState } = wp.element; const { createElement, useState, useEffect, useCallback } = wp.element;
const { TextControl } = wp.components; const { TextControl } = wp.components;
const { __ } = wp.i18n; const { __ } = wp.i18n;
// Get available exports from blocksCheckout // Get available exports from blocksCheckout
const { ExperimentalOrderMeta } = wc.blocksCheckout; const { ExperimentalOrderMeta, extensionCartUpdate } = wc.blocksCheckout;
// Debounce function for API updates
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Get settings from PHP // Get settings from PHP
const settings = getSetting('wc-licensed-product_data', {}); const settings = getSetting('wc-licensed-product_data', {});
@@ -59,6 +72,23 @@
const [domain, setDomain] = useState(''); const [domain, setDomain] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
// Debounced API update function
const updateStoreApi = useCallback(
debounce((normalizedDomain) => {
if (extensionCartUpdate) {
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domain: normalizedDomain,
},
}).catch(err => {
console.error('[WCLP] Store API update error:', err);
});
}
}, 500),
[]
);
const handleChange = (value) => { const handleChange = (value) => {
const normalized = normalizeDomain(value); const normalized = normalizeDomain(value);
setDomain(normalized); setDomain(normalized);
@@ -67,9 +97,11 @@
setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product')); setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'));
} else { } else {
setError(''); setError('');
// Update Store API when valid
updateStoreApi(normalized);
} }
// Store in hidden input for form submission // Store in hidden input for form submission (fallback)
const hiddenInput = document.getElementById('wclp-domain-hidden'); const hiddenInput = document.getElementById('wclp-domain-hidden');
if (hiddenInput) { if (hiddenInput) {
hiddenInput.value = normalized; hiddenInput.value = normalized;
@@ -135,6 +167,23 @@
}); });
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
// Debounced API update function
const updateStoreApi = useCallback(
debounce((domainsData) => {
if (extensionCartUpdate) {
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domains: domainsData,
},
}).catch(err => {
console.error('[WCLP] Store API update error:', err);
});
}
}, 500),
[]
);
if (!products.length) { if (!products.length) {
return null; return null;
} }
@@ -174,7 +223,7 @@
setErrors(newErrors); setErrors(newErrors);
// Update hidden field with variation support // Build domain data for Store API
const data = products.map(p => { const data = products.map(p => {
const pKey = getProductKey(p); const pKey = getProductKey(p);
const doms = newDomains[pKey] || []; const doms = newDomains[pKey] || [];
@@ -188,6 +237,10 @@
return entry; return entry;
}).filter(item => item.domains.length > 0); }).filter(item => item.domains.length > 0);
// Update Store API
updateStoreApi(data);
// Update hidden field (fallback)
const hiddenInput = document.getElementById('wclp-domains-hidden'); const hiddenInput = document.getElementById('wclp-domains-hidden');
if (hiddenInput) { if (hiddenInput) {
hiddenInput.value = JSON.stringify(data); hiddenInput.value = JSON.stringify(data);
@@ -273,11 +326,13 @@
if (registerPlugin) { if (registerPlugin) {
registerPlugin('wc-licensed-product-domain-fields', { registerPlugin('wc-licensed-product-domain-fields', {
render: () => createElement( render: () => {
return createElement(
ExperimentalOrderMeta, ExperimentalOrderMeta,
{}, {},
createElement(LicenseDomainsBlock) createElement(LicenseDomainsBlock)
), );
},
scope: 'woocommerce-checkout', scope: 'woocommerce-checkout',
}); });
} }
@@ -379,6 +434,68 @@
} else { } else {
insertionPoint.appendChild(container); insertionPoint.appendChild(container);
} }
// Add event listeners to sync with Store API
const debouncedUpdate = debounce(function() {
if (!extensionCartUpdate) {
return;
}
if (settings.isMultiDomainEnabled && settings.licensedProducts) {
// Collect multi-domain data
const domainsData = settings.licensedProducts.map(function(product) {
const productKey = product.variation_id && product.variation_id > 0
? product.product_id + '_' + product.variation_id
: String(product.product_id);
const domains = [];
for (let i = 0; i < product.quantity; i++) {
const input = container.querySelector('input[name="licensed_domains[' + productKey + '][' + i + ']"]');
if (input && input.value.trim()) {
domains.push(normalizeDomain(input.value));
}
}
const entry = {
product_id: product.product_id,
domains: domains,
};
if (product.variation_id && product.variation_id > 0) {
entry.variation_id = product.variation_id;
}
return entry;
}).filter(function(item) { return item.domains.length > 0; });
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domains: domainsData,
},
}).catch(function(err) {
console.error('[WCLP] Store API update error:', err);
});
} else {
// Single domain
const input = container.querySelector('input[name="licensed_product_domain"]');
if (input) {
const domain = normalizeDomain(input.value);
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domain: domain,
},
}).catch(function(err) {
console.error('[WCLP] Store API update error:', err);
});
}
}
}, 500);
// Attach event listeners to all domain inputs
container.querySelectorAll('input[type="text"]').forEach(function(input) {
input.addEventListener('input', debouncedUpdate);
input.addEventListener('change', debouncedUpdate);
});
}, 2000); }, 2000);
})(); })();

Binary file not shown.

View File

@@ -83,7 +83,7 @@ final class OrderLicenseController
$hasLicensedProduct = false; $hasLicensedProduct = false;
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if ($product && $product->is_type('licensed')) { if ($product && $this->licenseManager->isLicensedProduct($product)) {
$hasLicensedProduct = true; $hasLicensedProduct = true;
break; break;
} }
@@ -162,7 +162,7 @@ final class OrderLicenseController
// Legacy: one license per licensed product // Legacy: one license per licensed product
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if ($product && $product->is_type('licensed')) { if ($product && $this->licenseManager->isLicensedProduct($product)) {
$expectedLicenses++; $expectedLicenses++;
} }
} }
@@ -567,7 +567,7 @@ final class OrderLicenseController
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if (!$product || !$product->is_type('licensed')) { if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
continue; continue;
} }
@@ -615,7 +615,7 @@ final class OrderLicenseController
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if (!$product || !$product->is_type('licensed')) { if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
continue; continue;
} }

View File

@@ -112,10 +112,12 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
public function get_script_data(): array public function get_script_data(): array
{ {
$isMultiDomain = SettingsController::isMultiDomainEnabled(); $isMultiDomain = SettingsController::isMultiDomainEnabled();
$licensedProducts = $this->getLicensedProductsFromCart();
$hasLicensedProducts = !empty($licensedProducts);
return [ return [
'hasLicensedProducts' => $this->cartHasLicensedProducts(), 'hasLicensedProducts' => $hasLicensedProducts,
'licensedProducts' => $this->getLicensedProductsFromCart(), 'licensedProducts' => $licensedProducts,
'isMultiDomainEnabled' => $isMultiDomain, 'isMultiDomainEnabled' => $isMultiDomain,
'fieldPlaceholder' => __('example.com', 'wc-licensed-product'), 'fieldPlaceholder' => __('example.com', 'wc-licensed-product'),
'fieldDescription' => $isMultiDomain 'fieldDescription' => $isMultiDomain
@@ -151,8 +153,11 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
} }
$licensedProducts = []; $licensedProducts = [];
foreach (WC()->cart->get_cart() as $cartItem) { $cartContents = WC()->cart->get_cart();
foreach ($cartContents as $cartKey => $cartItem) {
$product = $cartItem['data']; $product = $cartItem['data'];
if (!$product) { if (!$product) {
continue; continue;
} }
@@ -171,11 +176,12 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
} }
// Check for variations of licensed-variable products // Check for variations of licensed-variable products
if ($product->is_type('variation')) { // Use WC_Product_Factory::get_product_type() for reliable parent type check
$parentId = $product->get_parent_id(); $parentId = $product->get_parent_id();
$parent = wc_get_product($parentId); if ($parentId) {
$parentType = \WC_Product_Factory::get_product_type($parentId);
if ($parent && $parent->is_type('licensed-variable')) { if ($parentType === 'licensed-variable') {
$variationId = $product->get_id(); $variationId = $product->get_id();
// Get duration label if it's a LicensedProductVariation // Get duration label if it's a LicensedProductVariation

View File

@@ -67,8 +67,9 @@ final class CheckoutController
} }
$licensedProducts = []; $licensedProducts = [];
foreach (WC()->cart->get_cart() as $cartItem) { foreach (WC()->cart->get_cart() as $cartItemKey => $cartItem) {
$product = $cartItem['data']; $product = $cartItem['data'];
if (!$product) { if (!$product) {
continue; continue;
} }
@@ -87,11 +88,12 @@ final class CheckoutController
} }
// Check for variations of licensed-variable products // Check for variations of licensed-variable products
if ($product->is_type('variation')) { // Use WC_Product_Factory::get_product_type() for reliable parent type check
$parentId = $product->get_parent_id(); $parentId = $product->get_parent_id();
$parent = wc_get_product($parentId); if ($parentId) {
$parentType = \WC_Product_Factory::get_product_type($parentId);
if ($parent && $parent->is_type('licensed-variable')) { if ($parentType === 'licensed-variable') {
$variationId = $product->get_id(); $variationId = $product->get_id();
// Use combination key to allow same product with different variations // Use combination key to allow same product with different variations
$key = "{$parentId}_{$variationId}"; $key = "{$parentId}_{$variationId}";
@@ -127,6 +129,7 @@ final class CheckoutController
public function addDomainField(): void public function addDomainField(): void
{ {
$licensedProducts = $this->getLicensedProductsFromCart(); $licensedProducts = $this->getLicensedProductsFromCart();
if (empty($licensedProducts)) { if (empty($licensedProducts)) {
return; return;
} }
@@ -401,6 +404,7 @@ final class CheckoutController
public function saveDomainField(int $orderId): void public function saveDomainField(int $orderId): void
{ {
$licensedProducts = $this->getLicensedProductsFromCart(); $licensedProducts = $this->getLicensedProductsFromCart();
if (empty($licensedProducts)) { if (empty($licensedProducts)) {
return; return;
} }

View File

@@ -37,10 +37,18 @@ class LicenseManager
return true; return true;
} }
// Check for our custom variation class
if ($product instanceof LicensedProductVariation) {
return true;
}
// Variation of a licensed-variable product // Variation of a licensed-variable product
if ($product->is_type('variation') && $product->get_parent_id()) { // Use WC_Product_Factory::get_product_type() for reliable parent type check
$parent = wc_get_product($product->get_parent_id()); // This queries the database directly and doesn't depend on product class loading
if ($parent && $parent->is_type('licensed-variable')) { $parentId = $product->get_parent_id();
if ($parentId) {
$parentType = \WC_Product_Factory::get_product_type($parentId);
if ($parentType === 'licensed-variable') {
return true; return true;
} }
} }
@@ -101,10 +109,10 @@ class LicenseManager
// For variations, load the variation; otherwise load the parent product // For variations, load the variation; otherwise load the parent product
if ($variationId) { if ($variationId) {
$settingsProduct = wc_get_product($variationId); $settingsProduct = wc_get_product($variationId);
$parentProduct = wc_get_product($productId);
// Verify parent is licensed-variable // Verify parent is licensed-variable using DB-level type check
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) { $parentType = \WC_Product_Factory::get_product_type($productId);
if ($parentType !== 'licensed-variable') {
return null; return null;
} }

View File

@@ -210,6 +210,7 @@ final class Plugin
// Try new multi-domain format first // Try new multi-domain format first
$domainData = $order->get_meta('_licensed_product_domains'); $domainData = $order->get_meta('_licensed_product_domains');
if (!empty($domainData) && is_array($domainData)) { if (!empty($domainData) && is_array($domainData)) {
$this->generateLicensesMultiDomain($order, $domainData); $this->generateLicensesMultiDomain($order, $domainData);
return; return;
@@ -244,7 +245,12 @@ final class Plugin
// Generate licenses for each licensed product // Generate licenses for each licensed product
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
if (!$product) {
continue;
}
if (!$this->licenseManager->isLicensedProduct($product)) {
continue; continue;
} }
@@ -278,12 +284,14 @@ final class Plugin
private function generateLicensesSingleDomain(\WC_Order $order): void private function generateLicensesSingleDomain(\WC_Order $order): void
{ {
$domain = $order->get_meta('_licensed_product_domain'); $domain = $order->get_meta('_licensed_product_domain');
if (empty($domain)) { if (empty($domain)) {
return; return;
} }
foreach ($order->get_items() as $item) { foreach ($order->get_items() as $item) {
$product = $item->get_product(); $product = $item->get_product();
if ($product && $this->licenseManager->isLicensedProduct($product)) { if ($product && $this->licenseManager->isLicensedProduct($product)) {
// Get the parent product ID (for variations, this is the main product) // Get the parent product ID (for variations, this is the main product)
$productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id(); $productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();

View File

@@ -3,7 +3,7 @@
* Plugin Name: WooCommerce Licensed Product * Plugin Name: WooCommerce Licensed Product
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation. * Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
* Version: 0.5.12 * Version: 0.5.13
* Author: Marco Graetsch * Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev * Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
} }
// Plugin constants // Plugin constants
define('WC_LICENSED_PRODUCT_VERSION', '0.5.12'); define('WC_LICENSED_PRODUCT_VERSION', '0.5.13');
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));