You've already forked wc-licensed-product
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:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -18,12 +18,25 @@
|
||||
}
|
||||
|
||||
const { getSetting } = wc.wcSettings;
|
||||
const { createElement, useState } = wp.element;
|
||||
const { createElement, useState, useEffect, useCallback } = wp.element;
|
||||
const { TextControl } = wp.components;
|
||||
const { __ } = wp.i18n;
|
||||
|
||||
// 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
|
||||
const settings = getSetting('wc-licensed-product_data', {});
|
||||
@@ -59,6 +72,23 @@
|
||||
const [domain, setDomain] = 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 normalized = normalizeDomain(value);
|
||||
setDomain(normalized);
|
||||
@@ -67,9 +97,11 @@
|
||||
setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'));
|
||||
} else {
|
||||
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');
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = normalized;
|
||||
@@ -135,6 +167,23 @@
|
||||
});
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@@ -174,7 +223,7 @@
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
// Update hidden field with variation support
|
||||
// Build domain data for Store API
|
||||
const data = products.map(p => {
|
||||
const pKey = getProductKey(p);
|
||||
const doms = newDomains[pKey] || [];
|
||||
@@ -188,6 +237,10 @@
|
||||
return entry;
|
||||
}).filter(item => item.domains.length > 0);
|
||||
|
||||
// Update Store API
|
||||
updateStoreApi(data);
|
||||
|
||||
// Update hidden field (fallback)
|
||||
const hiddenInput = document.getElementById('wclp-domains-hidden');
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = JSON.stringify(data);
|
||||
@@ -273,11 +326,13 @@
|
||||
|
||||
if (registerPlugin) {
|
||||
registerPlugin('wc-licensed-product-domain-fields', {
|
||||
render: () => createElement(
|
||||
ExperimentalOrderMeta,
|
||||
{},
|
||||
createElement(LicenseDomainsBlock)
|
||||
),
|
||||
render: () => {
|
||||
return createElement(
|
||||
ExperimentalOrderMeta,
|
||||
{},
|
||||
createElement(LicenseDomainsBlock)
|
||||
);
|
||||
},
|
||||
scope: 'woocommerce-checkout',
|
||||
});
|
||||
}
|
||||
@@ -379,6 +434,68 @@
|
||||
} else {
|
||||
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);
|
||||
|
||||
})();
|
||||
|
||||
BIN
releases/wc-licensed-product-0.5.13.zip
Normal file
BIN
releases/wc-licensed-product-0.5.13.zip
Normal file
Binary file not shown.
@@ -83,7 +83,7 @@ final class OrderLicenseController
|
||||
$hasLicensedProduct = false;
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
if ($product && $this->licenseManager->isLicensedProduct($product)) {
|
||||
$hasLicensedProduct = true;
|
||||
break;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ final class OrderLicenseController
|
||||
// Legacy: one license per licensed product
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
if ($product && $this->licenseManager->isLicensedProduct($product)) {
|
||||
$expectedLicenses++;
|
||||
}
|
||||
}
|
||||
@@ -567,7 +567,7 @@ final class OrderLicenseController
|
||||
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if (!$product || !$product->is_type('licensed')) {
|
||||
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ final class OrderLicenseController
|
||||
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if (!$product || !$product->is_type('licensed')) {
|
||||
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,10 +112,12 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
||||
public function get_script_data(): array
|
||||
{
|
||||
$isMultiDomain = SettingsController::isMultiDomainEnabled();
|
||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||
$hasLicensedProducts = !empty($licensedProducts);
|
||||
|
||||
return [
|
||||
'hasLicensedProducts' => $this->cartHasLicensedProducts(),
|
||||
'licensedProducts' => $this->getLicensedProductsFromCart(),
|
||||
'hasLicensedProducts' => $hasLicensedProducts,
|
||||
'licensedProducts' => $licensedProducts,
|
||||
'isMultiDomainEnabled' => $isMultiDomain,
|
||||
'fieldPlaceholder' => __('example.com', 'wc-licensed-product'),
|
||||
'fieldDescription' => $isMultiDomain
|
||||
@@ -151,8 +153,11 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
||||
}
|
||||
|
||||
$licensedProducts = [];
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
$cartContents = WC()->cart->get_cart();
|
||||
|
||||
foreach ($cartContents as $cartKey => $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
@@ -171,11 +176,12 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
||||
}
|
||||
|
||||
// Check for variations of licensed-variable products
|
||||
if ($product->is_type('variation')) {
|
||||
$parentId = $product->get_parent_id();
|
||||
$parent = wc_get_product($parentId);
|
||||
// Use WC_Product_Factory::get_product_type() for reliable parent type check
|
||||
$parentId = $product->get_parent_id();
|
||||
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();
|
||||
|
||||
// Get duration label if it's a LicensedProductVariation
|
||||
|
||||
@@ -67,8 +67,9 @@ final class CheckoutController
|
||||
}
|
||||
|
||||
$licensedProducts = [];
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
foreach (WC()->cart->get_cart() as $cartItemKey => $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
@@ -87,11 +88,12 @@ final class CheckoutController
|
||||
}
|
||||
|
||||
// Check for variations of licensed-variable products
|
||||
if ($product->is_type('variation')) {
|
||||
$parentId = $product->get_parent_id();
|
||||
$parent = wc_get_product($parentId);
|
||||
// Use WC_Product_Factory::get_product_type() for reliable parent type check
|
||||
$parentId = $product->get_parent_id();
|
||||
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();
|
||||
// Use combination key to allow same product with different variations
|
||||
$key = "{$parentId}_{$variationId}";
|
||||
@@ -127,6 +129,7 @@ final class CheckoutController
|
||||
public function addDomainField(): void
|
||||
{
|
||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||
|
||||
if (empty($licensedProducts)) {
|
||||
return;
|
||||
}
|
||||
@@ -401,6 +404,7 @@ final class CheckoutController
|
||||
public function saveDomainField(int $orderId): void
|
||||
{
|
||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||
|
||||
if (empty($licensedProducts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,10 +37,18 @@ class LicenseManager
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for our custom variation class
|
||||
if ($product instanceof LicensedProductVariation) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Variation of a licensed-variable product
|
||||
if ($product->is_type('variation') && $product->get_parent_id()) {
|
||||
$parent = wc_get_product($product->get_parent_id());
|
||||
if ($parent && $parent->is_type('licensed-variable')) {
|
||||
// Use WC_Product_Factory::get_product_type() for reliable parent type check
|
||||
// This queries the database directly and doesn't depend on product class loading
|
||||
$parentId = $product->get_parent_id();
|
||||
if ($parentId) {
|
||||
$parentType = \WC_Product_Factory::get_product_type($parentId);
|
||||
if ($parentType === 'licensed-variable') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -101,10 +109,10 @@ class LicenseManager
|
||||
// For variations, load the variation; otherwise load the parent product
|
||||
if ($variationId) {
|
||||
$settingsProduct = wc_get_product($variationId);
|
||||
$parentProduct = wc_get_product($productId);
|
||||
|
||||
// Verify parent is licensed-variable
|
||||
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
|
||||
// Verify parent is licensed-variable using DB-level type check
|
||||
$parentType = \WC_Product_Factory::get_product_type($productId);
|
||||
if ($parentType !== 'licensed-variable') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,7 @@ final class Plugin
|
||||
|
||||
// Try new multi-domain format first
|
||||
$domainData = $order->get_meta('_licensed_product_domains');
|
||||
|
||||
if (!empty($domainData) && is_array($domainData)) {
|
||||
$this->generateLicensesMultiDomain($order, $domainData);
|
||||
return;
|
||||
@@ -244,7 +245,12 @@ final class Plugin
|
||||
// Generate licenses for each licensed product
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
|
||||
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->licenseManager->isLicensedProduct($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -278,12 +284,14 @@ final class Plugin
|
||||
private function generateLicensesSingleDomain(\WC_Order $order): void
|
||||
{
|
||||
$domain = $order->get_meta('_licensed_product_domain');
|
||||
|
||||
if (empty($domain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if ($product && $this->licenseManager->isLicensedProduct($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();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WooCommerce 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.
|
||||
* Version: 0.5.12
|
||||
* Version: 0.5.13
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL-2.0-or-later
|
||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// 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_DIR', plugin_dir_path(__FILE__));
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user