diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c05a1..00157ae 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/assets/js/checkout-blocks.js b/assets/js/checkout-blocks.js index fbe72d9..9ba63eb 100644 --- a/assets/js/checkout-blocks.js +++ b/assets/js/checkout-blocks.js @@ -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); })(); diff --git a/releases/wc-licensed-product-0.5.13.zip b/releases/wc-licensed-product-0.5.13.zip new file mode 100644 index 0000000..61b6513 Binary files /dev/null and b/releases/wc-licensed-product-0.5.13.zip differ diff --git a/src/Admin/OrderLicenseController.php b/src/Admin/OrderLicenseController.php index ddfee45..363dd28 100644 --- a/src/Admin/OrderLicenseController.php +++ b/src/Admin/OrderLicenseController.php @@ -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; } diff --git a/src/Checkout/CheckoutBlocksIntegration.php b/src/Checkout/CheckoutBlocksIntegration.php index 6af8157..613d72c 100644 --- a/src/Checkout/CheckoutBlocksIntegration.php +++ b/src/Checkout/CheckoutBlocksIntegration.php @@ -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 diff --git a/src/Checkout/CheckoutController.php b/src/Checkout/CheckoutController.php index e8526b8..bc0408f 100644 --- a/src/Checkout/CheckoutController.php +++ b/src/Checkout/CheckoutController.php @@ -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; } diff --git a/src/License/LicenseManager.php b/src/License/LicenseManager.php index c64be1f..3f5c0f5 100644 --- a/src/License/LicenseManager.php +++ b/src/License/LicenseManager.php @@ -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; } diff --git a/src/Plugin.php b/src/Plugin.php index 7e0f6dc..51ea562 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -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(); diff --git a/wc-licensed-product.php b/wc-licensed-product.php index 78a1c47..f0d0ae2 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -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__));