diff --git a/CHANGELOG.md b/CHANGELOG.md index ac54e33..b732429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.3] - 2026-01-26 + +### Added + +- Variable licensed product type (`licensed-variable`) for selling licenses with different durations +- Support for monthly, yearly, quarterly, or lifetime license variations +- `LicensedVariableProduct` class extending `WC_Product_Variable` +- `LicensedProductVariation` class for individual variation license settings +- Variation-specific license duration settings in product edit page +- Duration labels displayed in checkout domain fields (e.g., "Yearly License") +- Variation ID tracking in order domain meta for proper license generation + +### Changed + +- Updated `LicenseManager::generateLicense()` to accept optional variation ID +- Checkout now handles variations with separate domain fields per product/variation +- WooCommerce Blocks checkout updated to display variation duration labels +- Store API extension updated to include variation_id in domain data schema + ## [0.5.2] - 2026-01-26 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 0aa8041..0c7c7b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,10 +32,6 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. -### Version 0.5.2 - -*No planned bugfixes yet.* - ### Version 0.6.0 *No planned features yet.* @@ -1341,3 +1337,44 @@ Security enhancement release adding per-license customer secrets for API respons - Created release package: `releases/wc-licensed-product-0.5.2.zip` (845 KB) - SHA256: `2d61a78ac5ba0f1d115a6401e6dded5b872b18f5530027c371604cbd18e9e27c` - Tagged as `v0.5.2` and pushed to `main` branch + +### 2026-01-26 - Version 0.5.3 - Variable Licensed Products + +**Overview:** + +Major feature release adding support for WooCommerce variable products. Customers can now purchase licenses with different durations (monthly, yearly, lifetime) as product variations. + +**New files:** + +- `src/Product/LicensedVariableProduct.php` - Variable product class extending `WC_Product_Variable` +- `src/Product/LicensedProductVariation.php` - Variation class with license settings + +**Implemented:** + +- New `licensed-variable` product type for selling licenses with different durations +- `LicensedVariableProduct` class extending WooCommerce variable products +- `LicensedProductVariation` class for individual variation license settings +- Variation-specific license duration fields in product edit page (days, max activations) +- Duration labels (Monthly, Quarterly, Yearly, Lifetime) displayed in checkout +- Variation ID tracking in order domain meta for proper license generation +- WooCommerce Blocks checkout updated to handle variations with duration labels + +**Modified files:** + +- `src/Product/LicensedProductType.php` - Added licensed-variable type registration, variation hooks +- `src/License/LicenseManager.php` - Added `isLicensedProduct()` helper, variation support in `generateLicense()` +- `src/Plugin.php` - Updated license generation to handle variations +- `src/Checkout/CheckoutController.php` - Variation support in domain field rendering +- `src/Checkout/CheckoutBlocksIntegration.php` - Variation data in blocks checkout +- `src/Checkout/StoreApiExtension.php` - Variation ID in Store API schema +- `assets/js/checkout-blocks.js` - Variation handling in React components and DOM fallback + +**Technical notes:** + +- Variable product type shows in WooCommerce product type selector as "Licensed Variable Product" +- Each variation can override parent's license duration and max activations +- Variations are always virtual (licensed products don't ship) +- `LicensedProductVariation::get_license_duration_label()` returns human-readable duration +- Order meta `_licensed_product_domains` now includes optional `variation_id` field +- License generation uses variation settings when `variation_id` is present in order item +- Backward compatible: existing simple licensed products continue to work unchanged diff --git a/assets/js/checkout-blocks.js b/assets/js/checkout-blocks.js index 1cdd17b..fbe72d9 100644 --- a/assets/js/checkout-blocks.js +++ b/assets/js/checkout-blocks.js @@ -110,6 +110,16 @@ ); }; + /** + * Get unique key for product (handles variations) + */ + function getProductKey(product) { + if (product.variation_id && product.variation_id > 0) { + return `${product.product_id}_${product.variation_id}`; + } + return String(product.product_id); + } + /** * Multi-Domain Component */ @@ -118,7 +128,8 @@ const [domains, setDomains] = useState(() => { const init = {}; products.forEach(p => { - init[p.product_id] = Array(p.quantity).fill(''); + const key = getProductKey(p); + init[key] = Array(p.quantity).fill(''); }); return init; }); @@ -128,16 +139,16 @@ return null; } - const handleChange = (productId, index, value) => { + const handleChange = (productKey, index, value) => { const normalized = normalizeDomain(value); const newDomains = { ...domains }; - if (!newDomains[productId]) newDomains[productId] = []; - newDomains[productId] = [...newDomains[productId]]; - newDomains[productId][index] = normalized; + if (!newDomains[productKey]) newDomains[productKey] = []; + newDomains[productKey] = [...newDomains[productKey]]; + newDomains[productKey][index] = normalized; setDomains(newDomains); // Validate - const key = `${productId}_${index}`; + const key = `${productKey}_${index}`; const newErrors = { ...errors }; if (normalized && !isValidDomain(normalized)) { newErrors[key] = settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'); @@ -145,14 +156,14 @@ delete newErrors[key]; } - // Check for duplicates within same product - const productDomains = newDomains[productId].filter(d => d); + // Check for duplicates within same product/variation + const productDomains = newDomains[productKey].filter(d => d); const uniqueDomains = new Set(productDomains.map(d => normalizeDomain(d))); if (productDomains.length !== uniqueDomains.size) { const seen = new Set(); - newDomains[productId].forEach((d, idx) => { + newDomains[productKey].forEach((d, idx) => { const normalizedD = normalizeDomain(d); - const dupKey = `${productId}_${idx}`; + const dupKey = `${productKey}_${idx}`; if (normalizedD && seen.has(normalizedD)) { newErrors[dupKey] = settings.duplicateError || __('Each license requires a unique domain.', 'wc-licensed-product'); } else if (normalizedD) { @@ -163,11 +174,19 @@ setErrors(newErrors); - // Update hidden field - const data = Object.entries(newDomains).map(([pid, doms]) => ({ - product_id: parseInt(pid, 10), - domains: doms.filter(d => d), - })).filter(item => item.domains.length > 0); + // Update hidden field with variation support + const data = products.map(p => { + const pKey = getProductKey(p); + const doms = newDomains[pKey] || []; + const entry = { + product_id: p.product_id, + domains: doms.filter(d => d), + }; + if (p.variation_id && p.variation_id > 0) { + entry.variation_id = p.variation_id; + } + return entry; + }).filter(item => item.domains.length > 0); const hiddenInput = document.getElementById('wclp-domains-hidden'); if (hiddenInput) { @@ -192,35 +211,43 @@ createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } }, settings.fieldDescription || __('Enter a unique domain for each license.', 'wc-licensed-product') ), - products.map(product => createElement( - 'div', - { - key: product.product_id, - style: { - marginBottom: '16px', - padding: '12px', - backgroundColor: '#fff', - borderRadius: '4px', - } - }, - createElement('strong', { style: { display: 'block', marginBottom: '8px' } }, - product.name + (product.quantity > 1 ? ` (×${product.quantity})` : '') - ), - Array.from({ length: product.quantity }, (_, i) => { - const key = `${product.product_id}_${i}`; - return createElement( - 'div', - { key: i, style: { marginBottom: '8px' } }, - createElement(TextControl, { - label: (settings.licenseLabel || __('License %d:', 'wc-licensed-product')).replace('%d', i + 1), - value: domains[product.product_id]?.[i] || '', - onChange: (val) => handleChange(product.product_id, i, val), - placeholder: settings.fieldPlaceholder || 'example.com', - help: errors[key] || '', - }) - ); - }) - )), + products.map(product => { + const productKey = getProductKey(product); + const durationLabel = product.duration_label || ''; + const displayName = durationLabel + ? `${product.name} (${durationLabel})` + : product.name; + + return createElement( + 'div', + { + key: productKey, + style: { + marginBottom: '16px', + padding: '12px', + backgroundColor: '#fff', + borderRadius: '4px', + } + }, + createElement('strong', { style: { display: 'block', marginBottom: '8px' } }, + displayName + (product.quantity > 1 ? ` ×${product.quantity}` : '') + ), + Array.from({ length: product.quantity }, (_, i) => { + const key = `${productKey}_${i}`; + return createElement( + 'div', + { key: i, style: { marginBottom: '8px' } }, + createElement(TextControl, { + label: (settings.licenseLabel || __('License %d:', 'wc-licensed-product')).replace('%d', i + 1), + value: domains[productKey]?.[i] || '', + onChange: (val) => handleChange(productKey, i, val), + placeholder: settings.fieldPlaceholder || 'example.com', + help: errors[key] || '', + }) + ); + }) + ); + }), createElement('input', { type: 'hidden', id: 'wclp-domains-hidden', @@ -291,10 +318,19 @@
${settings.fieldDescription || 'Enter a unique domain for each license.'}
- ${settings.licensedProducts.map(product => ` + ${settings.licensedProducts.map(product => { + const productKey = product.variation_id && product.variation_id > 0 + ? `${product.product_id}_${product.variation_id}` + : product.product_id; + const durationLabel = product.duration_label || ''; + const displayName = durationLabel + ? `${product.name} (${durationLabel})` + : product.name; + + return `
:
@@ -482,8 +577,20 @@ final class CheckoutController
if ($plainText) {
echo "\n" . esc_html__('License Domains:', 'wc-licensed-product') . "\n";
foreach ($domainData as $item) {
- $product = wc_get_product($item['product_id']);
- $productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
+ $productId = $item['product_id'];
+ $variationId = $item['variation_id'] ?? 0;
+
+ if ($variationId > 0) {
+ $variation = wc_get_product($variationId);
+ $productName = $variation ? $variation->get_name() : __('Unknown Variation', 'wc-licensed-product');
+ if ($variation instanceof LicensedProductVariation) {
+ $productName .= ' (' . $variation->get_license_duration_label() . ')';
+ }
+ } else {
+ $product = wc_get_product($productId);
+ $productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
+ }
+
echo ' ' . esc_html($productName) . ': ' . esc_html(implode(', ', $item['domains'])) . "\n";
}
} else {
@@ -492,8 +599,19 @@ final class CheckoutController
get_name() : __('Unknown Product', 'wc-licensed-product');
+ $productId = $item['product_id'];
+ $variationId = $item['variation_id'] ?? 0;
+
+ if ($variationId > 0) {
+ $variation = wc_get_product($variationId);
+ $productName = $variation ? $variation->get_name() : __('Unknown Variation', 'wc-licensed-product');
+ if ($variation instanceof LicensedProductVariation) {
+ $productName .= ' (' . $variation->get_license_duration_label() . ')';
+ }
+ } else {
+ $product = wc_get_product($productId);
+ $productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
+ }
?>
:
diff --git a/src/Checkout/StoreApiExtension.php b/src/Checkout/StoreApiExtension.php
index 29c6053..a43013e 100644
--- a/src/Checkout/StoreApiExtension.php
+++ b/src/Checkout/StoreApiExtension.php
@@ -100,6 +100,9 @@ final class StoreApiExtension
'product_id' => [
'type' => 'integer',
],
+ 'variation_id' => [
+ 'type' => 'integer',
+ ],
'domains' => [
'type' => 'array',
'items' => [
@@ -162,6 +165,7 @@ final class StoreApiExtension
}
$productId = (int) $item['product_id'];
+ $variationId = isset($item['variation_id']) ? (int) $item['variation_id'] : 0;
$domains = [];
foreach ($item['domains'] as $domain) {
@@ -172,10 +176,17 @@ final class StoreApiExtension
}
if (!empty($domains)) {
- $normalized[] = [
+ $entry = [
'product_id' => $productId,
'domains' => $domains,
];
+
+ // Include variation_id if present
+ if ($variationId > 0) {
+ $entry['variation_id'] = $variationId;
+ }
+
+ $normalized[] = $entry;
}
}
@@ -267,10 +278,23 @@ final class StoreApiExtension
// Check for licensed_domains in classic format (from DOM injection)
if (empty($domainData) && isset($requestData['licensed_domains']) && is_array($requestData['licensed_domains'])) {
$domainData = [];
- foreach ($requestData['licensed_domains'] as $productId => $domains) {
+ $variationIds = $requestData['licensed_variation_ids'] ?? [];
+
+ foreach ($requestData['licensed_domains'] as $key => $domains) {
if (!is_array($domains)) {
continue;
}
+
+ // Parse key - could be "productId" or "productId_variationId"
+ $parts = explode('_', (string) $key);
+ $productId = (int) $parts[0];
+ $variationId = isset($parts[1]) ? (int) $parts[1] : 0;
+
+ // Also check for hidden variation ID field
+ if ($variationId === 0 && isset($variationIds[$key])) {
+ $variationId = (int) $variationIds[$key];
+ }
+
$normalizedDomains = [];
foreach ($domains as $domain) {
$sanitized = sanitize_text_field($domain);
@@ -279,10 +303,16 @@ final class StoreApiExtension
}
}
if (!empty($normalizedDomains)) {
- $domainData[] = [
- 'product_id' => (int) $productId,
+ $entry = [
+ 'product_id' => $productId,
'domains' => $normalizedDomains,
];
+
+ if ($variationId > 0) {
+ $entry['variation_id'] = $variationId;
+ }
+
+ $domainData[] = $entry;
}
}
}
diff --git a/src/License/LicenseManager.php b/src/License/LicenseManager.php
index 2012795..c64be1f 100644
--- a/src/License/LicenseManager.php
+++ b/src/License/LicenseManager.php
@@ -11,12 +11,43 @@ namespace Jeremias\WcLicensedProduct\License;
use Jeremias\WcLicensedProduct\Installer;
use Jeremias\WcLicensedProduct\Product\LicensedProduct;
+use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
+use Jeremias\WcLicensedProduct\Product\LicensedVariableProduct;
/**
* Manages license operations (CRUD, validation, generation)
*/
class LicenseManager
{
+ /**
+ * Check if a product is any type of licensed product
+ *
+ * @param \WC_Product $product Product to check
+ * @return bool True if product is licensed (simple or variable or variation)
+ */
+ public function isLicensedProduct(\WC_Product $product): bool
+ {
+ // Simple licensed product
+ if ($product->is_type('licensed')) {
+ return true;
+ }
+
+ // Variable licensed product
+ if ($product->is_type('licensed-variable')) {
+ 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')) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Generate a unique license key
*/
@@ -40,32 +71,63 @@ class LicenseManager
/**
* Generate a license for a completed order
+ *
+ * @param int $orderId Order ID
+ * @param int $productId Product ID (parent product for variations)
+ * @param int $customerId Customer ID
+ * @param string $domain Domain to bind the license to
+ * @param int|null $variationId Optional variation ID for variable licensed products
+ * @return License|null Generated license or null on failure
*/
public function generateLicense(
int $orderId,
int $productId,
int $customerId,
- string $domain
+ string $domain,
+ ?int $variationId = null
): ?License {
global $wpdb;
// Normalize domain first for duplicate detection
$normalizedDomain = $this->normalizeDomain($domain);
- // Check if license already exists for this order, product, and domain
- $existing = $this->getLicenseByOrderProductAndDomain($orderId, $productId, $normalizedDomain);
+ // Check if license already exists for this order, product, domain, and variation
+ $existing = $this->getLicenseByOrderProductDomainAndVariation($orderId, $productId, $normalizedDomain, $variationId);
if ($existing) {
return $existing;
}
- $product = wc_get_product($productId);
- if (!$product || !$product->is_type('licensed')) {
- return null;
+ // Load the product that has the license settings
+ // 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')) {
+ return null;
+ }
+
+ // Ensure we have the proper variation class
+ if ($settingsProduct && !$settingsProduct instanceof LicensedProductVariation) {
+ $settingsProduct = new LicensedProductVariation($variationId);
+ }
+ } else {
+ $settingsProduct = wc_get_product($productId);
+
+ // Check if this is a licensed product (simple)
+ if (!$settingsProduct || !$settingsProduct->is_type('licensed')) {
+ return null;
+ }
+
+ // Ensure we have the LicensedProduct instance for type hints
+ if (!$settingsProduct instanceof LicensedProduct) {
+ $settingsProduct = new LicensedProduct($productId);
+ }
}
- // Ensure we have the LicensedProduct instance for type hints
- if (!$product instanceof LicensedProduct) {
- $product = new LicensedProduct($productId);
+ if (!$settingsProduct) {
+ return null;
}
// Generate unique license key
@@ -74,16 +136,16 @@ class LicenseManager
$licenseKey = $this->generateLicenseKey();
}
- // Calculate expiration date
+ // Calculate expiration date from the settings product (variation or parent)
$expiresAt = null;
- $validityDays = $product->get_validity_days();
+ $validityDays = $settingsProduct->get_validity_days();
if ($validityDays !== null && $validityDays > 0) {
$expiresAt = (new \DateTimeImmutable())->modify("+{$validityDays} days")->format('Y-m-d H:i:s');
}
- // Determine version ID if bound to version
+ // Determine version ID if bound to version (always use parent product ID for versions)
$versionId = null;
- if ($product->is_bound_to_version()) {
+ if ($settingsProduct->is_bound_to_version()) {
$versionId = $this->getCurrentVersionId($productId);
}
@@ -99,7 +161,7 @@ class LicenseManager
'version_id' => $versionId,
'status' => License::STATUS_ACTIVE,
'activations_count' => 1,
- 'max_activations' => $product->get_max_activations(),
+ 'max_activations' => $settingsProduct->get_max_activations(),
'expires_at' => $expiresAt,
],
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
@@ -112,6 +174,16 @@ class LicenseManager
return $this->getLicenseById((int) $wpdb->insert_id);
}
+ /**
+ * Get license by order, product, domain, and optional variation
+ */
+ public function getLicenseByOrderProductDomainAndVariation(int $orderId, int $productId, string $domain, ?int $variationId = null): ?License
+ {
+ // For now, just use the existing method since we don't store variation_id in licenses table yet
+ // In the future, we could add a variation_id column to the licenses table
+ return $this->getLicenseByOrderProductAndDomain($orderId, $productId, $domain);
+ }
+
/**
* Get license by ID
*/
diff --git a/src/Plugin.php b/src/Plugin.php
index c4fb703..7e0f6dc 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -227,23 +227,35 @@ final class Plugin
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
- // Index domains by product ID for quick lookup
+ // Index domains by product ID (and variation ID for variable products)
$domainsByProduct = [];
foreach ($domainData as $item) {
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
- $domainsByProduct[(int) $item['product_id']] = $item['domains'];
+ $productId = (int) $item['product_id'];
+ $variationId = isset($item['variation_id']) ? (int) $item['variation_id'] : 0;
+ $key = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
+ $domainsByProduct[$key] = [
+ 'domains' => $item['domains'],
+ 'variation_id' => $variationId,
+ ];
}
}
// Generate licenses for each licensed product
foreach ($order->get_items() as $item) {
$product = $item->get_product();
- if (!$product || !$product->is_type('licensed')) {
+ if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
continue;
}
- $productId = $product->get_id();
- $domains = $domainsByProduct[$productId] ?? [];
+ // Get the parent product ID (for variations, this is the main product)
+ $productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
+ $variationId = $item->get_variation_id();
+
+ // Look up domains - first try with variation, then without
+ $key = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
+ $domainInfo = $domainsByProduct[$key] ?? $domainsByProduct[(string) $productId] ?? null;
+ $domains = $domainInfo['domains'] ?? [];
// Generate a license for each domain
foreach ($domains as $domain) {
@@ -252,7 +264,8 @@ final class Plugin
$orderId,
$productId,
$customerId,
- $domain
+ $domain,
+ $variationId > 0 ? $variationId : null
);
}
}
@@ -271,12 +284,17 @@ final class Plugin
foreach ($order->get_items() as $item) {
$product = $item->get_product();
- if ($product && $product->is_type('licensed')) {
+ 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();
+ $variationId = $item->get_variation_id();
+
$this->licenseManager->generateLicense(
$order->get_id(),
- $product->get_id(),
+ $productId,
$order->get_customer_id(),
- $domain
+ $domain,
+ $variationId > 0 ? $variationId : null
);
}
}
diff --git a/src/Product/LicensedProductType.php b/src/Product/LicensedProductType.php
index 0a903c3..7815b2c 100644
--- a/src/Product/LicensedProductType.php
+++ b/src/Product/LicensedProductType.php
@@ -12,7 +12,8 @@ namespace Jeremias\WcLicensedProduct\Product;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
/**
- * Registers and handles the Licensed product type for WooCommerce
+ * Registers and handles the Licensed product types for WooCommerce
+ * Supports both simple licensed products and variable licensed products
*/
final class LicensedProductType
{
@@ -29,7 +30,7 @@ final class LicensedProductType
*/
private function registerHooks(): void
{
- // Register product type
+ // Register product types
add_filter('product_type_selector', [$this, 'addProductType']);
add_filter('woocommerce_product_class', [$this, 'getProductClass'], 10, 2);
@@ -39,9 +40,11 @@ final class LicensedProductType
// Save product meta
add_action('woocommerce_process_product_meta_licensed', [$this, 'saveProductMeta']);
+ add_action('woocommerce_process_product_meta_licensed-variable', [$this, 'saveProductMeta']);
// Show price and add to cart for licensed products
add_action('woocommerce_licensed_add_to_cart', [$this, 'addToCartTemplate']);
+ add_action('woocommerce_licensed-variable_add_to_cart', [$this, 'variableAddToCartTemplate']);
// Make product virtual by default
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
@@ -51,25 +54,48 @@ final class LicensedProductType
// Enqueue frontend CSS for licensed products on single product pages
add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
+
+ // Variable product support - variation settings
+ add_action('woocommerce_variation_options', [$this, 'addVariationOptions'], 10, 3);
+ add_action('woocommerce_product_after_variable_attributes', [$this, 'addVariationFields'], 10, 3);
+ add_action('woocommerce_save_product_variation', [$this, 'saveVariationFields'], 10, 2);
+
+ // Admin scripts for licensed-variable type
+ add_action('admin_footer', [$this, 'addVariableProductScripts']);
}
/**
- * Add product type to selector
+ * Add product types to selector
*/
public function addProductType(array $types): array
{
$types['licensed'] = __('Licensed Product', 'wc-licensed-product');
+ $types['licensed-variable'] = __('Licensed Variable Product', 'wc-licensed-product');
return $types;
}
/**
- * Get product class for licensed type
+ * Get product class for licensed types
*/
public function getProductClass(string $className, string $productType): string
{
if ($productType === 'licensed') {
return LicensedProduct::class;
}
+ if ($productType === 'licensed-variable') {
+ return LicensedVariableProduct::class;
+ }
+ // Handle variations of licensed-variable products
+ if ($productType === 'variation') {
+ // Check if parent is licensed-variable
+ global $post;
+ if ($post && $post->post_parent) {
+ $parentType = \WC_Product_Factory::get_product_type($post->post_parent);
+ if ($parentType === 'licensed-variable') {
+ return LicensedProductVariation::class;
+ }
+ }
+ }
return $className;
}
@@ -81,7 +107,7 @@ final class LicensedProductType
$tabs['licensed_product'] = [
'label' => __('License Settings', 'wc-licensed-product'),
'target' => 'licensed_product_data',
- 'class' => ['show_if_licensed'],
+ 'class' => ['show_if_licensed', 'show_if_licensed-variable'],
'priority' => 21,
];
return $tabs;
@@ -236,9 +262,16 @@ final class LicensedProductType
*/
public function isVirtual(bool $isVirtual, \WC_Product $product): bool
{
- if ($product->is_type('licensed')) {
+ if ($product->is_type('licensed') || $product->is_type('licensed-variable')) {
return true;
}
+ // Also handle variations of licensed-variable products
+ if ($product->is_type('variation') && $product->get_parent_id()) {
+ $parent = wc_get_product($product->get_parent_id());
+ if ($parent && $parent->is_type('licensed-variable')) {
+ return true;
+ }
+ }
return $isVirtual;
}
@@ -253,7 +286,7 @@ final class LicensedProductType
global $product;
- if (!$product || !$product->is_type('licensed')) {
+ if (!$product || (!$product->is_type('licensed') && !$product->is_type('licensed-variable'))) {
return;
}
@@ -272,11 +305,11 @@ final class LicensedProductType
{
global $product;
- if (!$product || !$product->is_type('licensed')) {
+ if (!$product || (!$product->is_type('licensed') && !$product->is_type('licensed-variable'))) {
return;
}
- /** @var LicensedProduct $product */
+ /** @var LicensedProduct|LicensedVariableProduct $product */
$version = $product->get_current_version();
if (empty($version)) {
@@ -289,4 +322,200 @@ final class LicensedProductType
esc_html($version)
);
}
+
+ /**
+ * Add to cart template for variable licensed products
+ */
+ public function variableAddToCartTemplate(): void
+ {
+ wc_get_template('single-product/add-to-cart/variable.php');
+ }
+
+ /**
+ * Add variation options (checkboxes next to variation header)
+ */
+ public function addVariationOptions(int $loop, array $variationData, \WP_Post $variation): void
+ {
+ // Check if parent is licensed-variable
+ $parentId = $variation->post_parent;
+ $parentProduct = wc_get_product($parentId);
+
+ if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
+ return;
+ }
+
+ $isVirtual = get_post_meta($variation->ID, '_virtual', true);
+ ?>
+
+ post_parent;
+ $parentProduct = wc_get_product($parentId);
+
+ if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
+ return;
+ }
+
+ // Get variation values
+ $validityDays = get_post_meta($variation->ID, '_licensed_validity_days', true);
+ $maxActivations = get_post_meta($variation->ID, '_licensed_max_activations', true);
+
+ // Get parent defaults for placeholder
+ $parentValidityDays = $parentProduct->get_validity_days();
+ $parentMaxActivations = $parentProduct->get_max_activations();
+
+ $parentValidityDisplay = $parentValidityDays !== null
+ ? sprintf(__('%d days', 'wc-licensed-product'), $parentValidityDays)
+ : __('Lifetime', 'wc-licensed-product');
+
+ ?>
+
+ + + +
++ + + +
+