You've already forked wc-licensed-product
Add licensed variable product support for duration-based licenses (v0.5.3)
Customers can now purchase licenses with different durations (monthly, yearly, lifetime) through WooCommerce product variations. Each variation can have its own license validity settings. New features: - LicensedVariableProduct class for variable licensed products - LicensedProductVariation class for individual variations - Per-variation license duration and max activations settings - Duration labels in checkout (Monthly, Quarterly, Yearly, etc.) - Full support for WooCommerce Blocks checkout with variations - Updated translations for German (de_CH) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ namespace Jeremias\WcLicensedProduct\Checkout;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
|
||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||
use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
|
||||
|
||||
/**
|
||||
* Integration with WooCommerce Checkout Blocks
|
||||
@@ -141,7 +142,7 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
||||
/**
|
||||
* Get licensed products from cart with quantities
|
||||
*
|
||||
* @return array<int, array{product_id: int, name: string, quantity: int}>
|
||||
* @return array<string, array{product_id: int, variation_id: int, name: string, quantity: int, duration_label: string}>
|
||||
*/
|
||||
private function getLicensedProductsFromCart(): array
|
||||
{
|
||||
@@ -152,13 +153,49 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
||||
$licensedProducts = [];
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for simple licensed products
|
||||
if ($product->is_type('licensed')) {
|
||||
$productId = $product->get_id();
|
||||
$licensedProducts[] = [
|
||||
'product_id' => $productId,
|
||||
'variation_id' => 0,
|
||||
'name' => $product->get_name(),
|
||||
'quantity' => (int) $cartItem['quantity'],
|
||||
'duration_label' => '',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for variations of licensed-variable products
|
||||
if ($product->is_type('variation')) {
|
||||
$parentId = $product->get_parent_id();
|
||||
$parent = wc_get_product($parentId);
|
||||
|
||||
if ($parent && $parent->is_type('licensed-variable')) {
|
||||
$variationId = $product->get_id();
|
||||
|
||||
// Get duration label if it's a LicensedProductVariation
|
||||
$durationLabel = '';
|
||||
if ($product instanceof LicensedProductVariation) {
|
||||
$durationLabel = $product->get_license_duration_label();
|
||||
} else {
|
||||
// Try to instantiate as LicensedProductVariation
|
||||
$variation = new LicensedProductVariation($variationId);
|
||||
$durationLabel = $variation->get_license_duration_label();
|
||||
}
|
||||
|
||||
$licensedProducts[] = [
|
||||
'product_id' => $parentId,
|
||||
'variation_id' => $variationId,
|
||||
'name' => $product->get_name(),
|
||||
'quantity' => (int) $cartItem['quantity'],
|
||||
'duration_label' => $durationLabel,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Jeremias\WcLicensedProduct\Checkout;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||
use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
|
||||
|
||||
/**
|
||||
* Handles checkout modifications for licensed products
|
||||
@@ -57,7 +58,7 @@ final class CheckoutController
|
||||
/**
|
||||
* Get licensed products from cart with quantities
|
||||
*
|
||||
* @return array<int, array{product_id: int, name: string, quantity: int}>
|
||||
* @return array<string, array{product_id: int, variation_id: int, name: string, quantity: int, duration_label: string}>
|
||||
*/
|
||||
private function getLicensedProductsFromCart(): array
|
||||
{
|
||||
@@ -68,13 +69,51 @@ final class CheckoutController
|
||||
$licensedProducts = [];
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
if (!$product) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for simple licensed products
|
||||
if ($product->is_type('licensed')) {
|
||||
$productId = $product->get_id();
|
||||
$licensedProducts[$productId] = [
|
||||
'product_id' => $productId,
|
||||
'variation_id' => 0,
|
||||
'name' => $product->get_name(),
|
||||
'quantity' => (int) $cartItem['quantity'],
|
||||
'duration_label' => '',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for variations of licensed-variable products
|
||||
if ($product->is_type('variation')) {
|
||||
$parentId = $product->get_parent_id();
|
||||
$parent = wc_get_product($parentId);
|
||||
|
||||
if ($parent && $parent->is_type('licensed-variable')) {
|
||||
$variationId = $product->get_id();
|
||||
// Use combination key to allow same product with different variations
|
||||
$key = "{$parentId}_{$variationId}";
|
||||
|
||||
// Get duration label if it's a LicensedProductVariation
|
||||
$durationLabel = '';
|
||||
if ($product instanceof LicensedProductVariation) {
|
||||
$durationLabel = $product->get_license_duration_label();
|
||||
} else {
|
||||
// Try to instantiate as LicensedProductVariation
|
||||
$variation = new LicensedProductVariation($variationId);
|
||||
$durationLabel = $variation->get_license_duration_label();
|
||||
}
|
||||
|
||||
$licensedProducts[$key] = [
|
||||
'product_id' => $parentId,
|
||||
'variation_id' => $variationId,
|
||||
'name' => $product->get_name(),
|
||||
'quantity' => (int) $cartItem['quantity'],
|
||||
'duration_label' => $durationLabel,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,22 +189,32 @@ final class CheckoutController
|
||||
<?php esc_html_e('Enter a unique domain for each license (without http:// or www).', 'wc-licensed-product'); ?>
|
||||
</p>
|
||||
|
||||
<?php foreach ($licensedProducts as $productId => $productData): ?>
|
||||
<div class="wclp-product-domains" data-product-id="<?php echo esc_attr($productId); ?>">
|
||||
<?php foreach ($licensedProducts as $key => $productData): ?>
|
||||
<?php
|
||||
$productId = $productData['product_id'];
|
||||
$variationId = $productData['variation_id'] ?? 0;
|
||||
$durationLabel = $productData['duration_label'] ?? '';
|
||||
// Use key for field names to handle variations
|
||||
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : $productId;
|
||||
?>
|
||||
<div class="wclp-product-domains" data-product-id="<?php echo esc_attr($productId); ?>" data-variation-id="<?php echo esc_attr($variationId); ?>">
|
||||
<h4>
|
||||
<?php
|
||||
echo esc_html($productData['name']);
|
||||
if (!empty($durationLabel)) {
|
||||
echo ' <span class="wclp-duration-badge">(' . esc_html($durationLabel) . ')</span>';
|
||||
}
|
||||
if ($productData['quantity'] > 1) {
|
||||
printf(' (×%d)', $productData['quantity']);
|
||||
printf(' ×%d', $productData['quantity']);
|
||||
}
|
||||
?>
|
||||
</h4>
|
||||
|
||||
<?php for ($i = 0; $i < $productData['quantity']; $i++): ?>
|
||||
<?php
|
||||
$fieldName = sprintf('licensed_domains[%d][%d]', $productId, $i);
|
||||
$fieldId = sprintf('licensed_domain_%d_%d', $productId, $i);
|
||||
$savedValue = $this->getSavedDomainValue($productId, $i);
|
||||
$fieldName = sprintf('licensed_domains[%s][%d]', $fieldKey, $i);
|
||||
$fieldId = sprintf('licensed_domain_%s_%d', str_replace('_', '-', $fieldKey), $i);
|
||||
$savedValue = $this->getSavedDomainValue($productId, $i, $variationId);
|
||||
?>
|
||||
<p class="form-row form-row-wide wclp-domain-row">
|
||||
<label for="<?php echo esc_attr($fieldId); ?>">
|
||||
@@ -186,6 +235,9 @@ final class CheckoutController
|
||||
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
|
||||
value="<?php echo esc_attr($savedValue); ?>"
|
||||
/>
|
||||
<?php if ($variationId > 0): ?>
|
||||
<input type="hidden" name="licensed_variation_ids[<?php echo esc_attr($fieldKey); ?>]" value="<?php echo esc_attr($variationId); ?>" />
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
@@ -197,6 +249,7 @@ final class CheckoutController
|
||||
.wclp-domain-description { margin-bottom: 15px; color: #666; }
|
||||
.wclp-product-domains { margin-bottom: 20px; padding: 15px; background: #f8f8f8; border-radius: 4px; }
|
||||
.wclp-product-domains h4 { margin: 0 0 10px 0; font-size: 1em; }
|
||||
.wclp-duration-badge { color: #0073aa; font-weight: normal; }
|
||||
.wclp-domain-row { margin-bottom: 10px; }
|
||||
.wclp-domain-row:last-child { margin-bottom: 0; }
|
||||
.wclp-domain-row label { display: block; margin-bottom: 5px; }
|
||||
@@ -207,9 +260,17 @@ final class CheckoutController
|
||||
/**
|
||||
* Get saved domain value from session/POST
|
||||
*/
|
||||
private function getSavedDomainValue(int $productId, int $index): string
|
||||
private function getSavedDomainValue(int $productId, int $index, int $variationId = 0): string
|
||||
{
|
||||
// Build the field key (with or without variation)
|
||||
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||
|
||||
// Check POST data first (validation failure case)
|
||||
if (isset($_POST['licensed_domains'][$fieldKey][$index])) {
|
||||
return sanitize_text_field($_POST['licensed_domains'][$fieldKey][$index]);
|
||||
}
|
||||
|
||||
// Also try numeric key for backward compatibility
|
||||
if (isset($_POST['licensed_domains'][$productId][$index])) {
|
||||
return sanitize_text_field($_POST['licensed_domains'][$productId][$index]);
|
||||
}
|
||||
@@ -218,7 +279,11 @@ final class CheckoutController
|
||||
if (WC()->session) {
|
||||
$sessionDomains = WC()->session->get('licensed_product_domains', []);
|
||||
foreach ($sessionDomains as $item) {
|
||||
if (isset($item['product_id']) && (int) $item['product_id'] === $productId) {
|
||||
$itemProductId = (int) ($item['product_id'] ?? 0);
|
||||
$itemVariationId = (int) ($item['variation_id'] ?? 0);
|
||||
|
||||
// Match by product and variation
|
||||
if ($itemProductId === $productId && $itemVariationId === $variationId) {
|
||||
if (isset($item['domains'][$index])) {
|
||||
return $item['domains'][$index];
|
||||
}
|
||||
@@ -272,8 +337,12 @@ final class CheckoutController
|
||||
{
|
||||
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
||||
|
||||
foreach ($licensedProducts as $productId => $productData) {
|
||||
$productDomains = $licensedDomains[$productId] ?? [];
|
||||
foreach ($licensedProducts as $key => $productData) {
|
||||
$productId = $productData['product_id'];
|
||||
$variationId = $productData['variation_id'] ?? 0;
|
||||
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||
|
||||
$productDomains = $licensedDomains[$fieldKey] ?? $licensedDomains[$productId] ?? [];
|
||||
$normalizedDomains = [];
|
||||
|
||||
for ($i = 0; $i < $productData['quantity']; $i++) {
|
||||
@@ -308,7 +377,7 @@ final class CheckoutController
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for duplicate domains within same product
|
||||
// Check for duplicate domains within same product/variation
|
||||
if (in_array($normalizedDomain, $normalizedDomains, true)) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
@@ -369,10 +438,15 @@ final class CheckoutController
|
||||
private function saveMultiDomainFields(\WC_Order $order, array $licensedProducts): void
|
||||
{
|
||||
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
||||
$licensedVariationIds = $_POST['licensed_variation_ids'] ?? [];
|
||||
$domainData = [];
|
||||
|
||||
foreach ($licensedProducts as $productId => $productData) {
|
||||
$productDomains = $licensedDomains[$productId] ?? [];
|
||||
foreach ($licensedProducts as $key => $productData) {
|
||||
$productId = $productData['product_id'];
|
||||
$variationId = $productData['variation_id'] ?? 0;
|
||||
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||
|
||||
$productDomains = $licensedDomains[$fieldKey] ?? $licensedDomains[$productId] ?? [];
|
||||
$normalizedDomains = [];
|
||||
|
||||
for ($i = 0; $i < $productData['quantity']; $i++) {
|
||||
@@ -383,10 +457,17 @@ final class CheckoutController
|
||||
}
|
||||
|
||||
if (!empty($normalizedDomains)) {
|
||||
$domainData[] = [
|
||||
$entry = [
|
||||
'product_id' => $productId,
|
||||
'domains' => $normalizedDomains,
|
||||
];
|
||||
|
||||
// Include variation_id if present
|
||||
if ($variationId > 0) {
|
||||
$entry['variation_id'] = $variationId;
|
||||
}
|
||||
|
||||
$domainData[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,8 +513,22 @@ final class CheckoutController
|
||||
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
|
||||
<?php foreach ($domainData as $item): ?>
|
||||
<?php
|
||||
$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;
|
||||
|
||||
// Get product name
|
||||
if ($variationId > 0) {
|
||||
$variation = wc_get_product($variationId);
|
||||
$productName = $variation ? $variation->get_name() : __('Unknown Variation', 'wc-licensed-product');
|
||||
|
||||
// Add duration label if available
|
||||
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');
|
||||
}
|
||||
?>
|
||||
<p style="margin: 5px 0 5px 15px;">
|
||||
<em><?php echo esc_html($productName); ?>:</em><br>
|
||||
@@ -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
|
||||
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
|
||||
<?php foreach ($domainData as $item): ?>
|
||||
<?php
|
||||
$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');
|
||||
}
|
||||
?>
|
||||
<p style="margin: 5px 0 5px 15px;">
|
||||
<em><?php echo esc_html($productName); ?>:</em><br>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user