Implement multi-domain licensing for v0.5.0

- Add multi-domain checkout support for WooCommerce Blocks
- Fix domain field rendering using ExperimentalOrderMeta slot
- Add DOM injection fallback for checkout field rendering
- Update translations with new multi-domain strings (de_CH)
- Update email templates for grouped license display
- Refactor account page to group licenses by product/order

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 18:31:36 +01:00
parent 550a84beb9
commit 83836d69af
16 changed files with 3816 additions and 2134 deletions

View File

@@ -94,8 +94,10 @@ final class OrderLicenseController
return;
}
// Get order domain
$orderDomain = $order->get_meta('_licensed_product_domain');
// Check for multi-domain format first, then fall back to legacy single domain
$multiDomainData = $order->get_meta('_licensed_product_domains');
$legacyDomain = $order->get_meta('_licensed_product_domain');
$hasMultiDomain = !empty($multiDomainData) && is_array($multiDomainData);
// Get licenses for this order
$licenses = $this->licenseManager->getLicensesByOrder($order->get_id());
@@ -104,23 +106,42 @@ final class OrderLicenseController
?>
<div class="wclp-order-licenses">
<div class="wclp-order-domain-section">
<h4><?php esc_html_e('Order Domain', 'wc-licensed-product'); ?></h4>
<p class="description">
<?php esc_html_e('The domain specified during checkout. Changing this will not automatically update existing license domains.', 'wc-licensed-product'); ?>
</p>
<div class="wclp-inline-edit">
<input type="text"
id="wclp-order-domain"
class="regular-text"
value="<?php echo esc_attr($orderDomain); ?>"
data-order-id="<?php echo esc_attr($order->get_id()); ?>"
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>" />
<button type="button" class="button" id="wclp-save-order-domain">
<?php esc_html_e('Save', 'wc-licensed-product'); ?>
</button>
<span class="spinner"></span>
<span class="wclp-status-message"></span>
</div>
<h4><?php esc_html_e('Order Domains', 'wc-licensed-product'); ?></h4>
<?php if ($hasMultiDomain): ?>
<p class="description">
<?php esc_html_e('Domains specified during checkout (multi-domain order).', 'wc-licensed-product'); ?>
</p>
<div class="wclp-multi-domain-display" style="margin-top: 10px;">
<?php foreach ($multiDomainData as $item): ?>
<?php
$product = wc_get_product($item['product_id']);
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
?>
<div class="wclp-product-domains-item" style="margin-bottom: 10px; padding: 10px; background: #f8f8f8; border-radius: 4px;">
<strong><?php echo esc_html($productName); ?>:</strong><br>
<code><?php echo esc_html(implode(', ', $item['domains'])); ?></code>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="description">
<?php esc_html_e('The domain specified during checkout. Changing this will not automatically update existing license domains.', 'wc-licensed-product'); ?>
</p>
<div class="wclp-inline-edit">
<input type="text"
id="wclp-order-domain"
class="regular-text"
value="<?php echo esc_attr($legacyDomain); ?>"
data-order-id="<?php echo esc_attr($order->get_id()); ?>"
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>" />
<button type="button" class="button" id="wclp-save-order-domain">
<?php esc_html_e('Save', 'wc-licensed-product'); ?>
</button>
<span class="spinner"></span>
<span class="wclp-status-message"></span>
</div>
<?php endif; ?>
</div>
<hr />
@@ -128,15 +149,26 @@ final class OrderLicenseController
<h4><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h4>
<?php
// Count licensed products to check if all have licenses
$licensedProductCount = 0;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$licensedProductCount++;
// Count expected licenses based on domain data
$expectedLicenses = 0;
if ($hasMultiDomain) {
// Multi-domain: count total domains across all products
foreach ($multiDomainData as $item) {
if (isset($item['domains']) && is_array($item['domains'])) {
$expectedLicenses += count($item['domains']);
}
}
} else {
// Legacy: one license per licensed product
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$expectedLicenses++;
}
}
}
$missingLicenses = $licensedProductCount - count($licenses);
$missingLicenses = $expectedLicenses - count($licenses);
$hasDomainData = $hasMultiDomain || !empty($legacyDomain);
?>
<?php if (empty($licenses)): ?>
@@ -150,7 +182,7 @@ final class OrderLicenseController
<em><?php esc_html_e('Licenses will be generated when the order is marked as paid/completed.', 'wc-licensed-product'); ?></em>
<?php endif; ?>
</p>
<?php if ($orderDomain && $order->is_paid()): ?>
<?php if ($hasDomainData && $order->is_paid()): ?>
<p style="margin-top: 10px;">
<button type="button" class="button button-primary" id="wclp-generate-licenses" data-order-id="<?php echo esc_attr($order->get_id()); ?>">
<?php esc_html_e('Generate Licenses', 'wc-licensed-product'); ?>
@@ -158,7 +190,7 @@ final class OrderLicenseController
<span class="spinner" style="float: none; margin-top: 4px;"></span>
<span class="wclp-generate-status"></span>
</p>
<?php elseif (!$orderDomain): ?>
<?php elseif (!$hasDomainData): ?>
<p class="description" style="margin-top: 10px; color: #d63638;">
<span class="dashicons dashicons-warning"></span>
<?php esc_html_e('Please set the order domain above before generating licenses.', 'wc-licensed-product'); ?>
@@ -251,7 +283,7 @@ final class OrderLicenseController
?>
</p>
<?php if ($missingLicenses > 0 && $orderDomain && $order->is_paid()): ?>
<?php if ($missingLicenses > 0 && $hasDomainData && $order->is_paid()): ?>
<p style="margin-top: 10px;">
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
<?php
@@ -474,68 +506,138 @@ final class OrderLicenseController
wp_send_json_error(['message' => __('Order must be paid before licenses can be generated.', 'wc-licensed-product')]);
}
// Get domain
$domain = $order->get_meta('_licensed_product_domain');
if (empty($domain)) {
// Check for multi-domain format first
$multiDomainData = $order->get_meta('_licensed_product_domains');
$legacyDomain = $order->get_meta('_licensed_product_domain');
if (!empty($multiDomainData) && is_array($multiDomainData)) {
// Multi-domain format
$result = $this->generateMultiDomainLicenses($order, $multiDomainData);
} elseif (!empty($legacyDomain)) {
// Legacy single domain format
$result = $this->generateLegacyLicenses($order, $legacyDomain);
} else {
wp_send_json_error(['message' => __('Please set the order domain before generating licenses.', 'wc-licensed-product')]);
return;
}
// Generate licenses for each licensed product
$generated = 0;
$skipped = 0;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$license = $this->licenseManager->generateLicense(
$orderId,
$product->get_id(),
$order->get_customer_id(),
$domain
);
if ($license) {
// Check if this is a new license or existing
$existingLicenses = $this->licenseManager->getLicensesByOrder($orderId);
$isNew = true;
foreach ($existingLicenses as $existing) {
if ($existing->getProductId() === $product->get_id() && $existing->getId() !== $license->getId()) {
$isNew = false;
break;
}
}
if ($isNew) {
$generated++;
} else {
$skipped++;
}
}
}
}
if ($generated > 0) {
if ($result['generated'] > 0) {
wp_send_json_success([
'message' => sprintf(
/* translators: %d: Number of licenses generated */
_n(
'%d license generated successfully.',
'%d licenses generated successfully.',
$generated,
$result['generated'],
'wc-licensed-product'
),
$generated
$result['generated']
),
'generated' => $generated,
'skipped' => $skipped,
'generated' => $result['generated'],
'skipped' => $result['skipped'],
'reload' => true,
]);
} else {
wp_send_json_success([
'message' => __('All licenses already exist for this order.', 'wc-licensed-product'),
'generated' => 0,
'skipped' => $skipped,
'skipped' => $result['skipped'],
'reload' => false,
]);
}
}
/**
* Generate licenses for multi-domain format
*/
private function generateMultiDomainLicenses(\WC_Order $order, array $domainData): array
{
$generated = 0;
$skipped = 0;
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
// Index domains by product ID
$domainsByProduct = [];
foreach ($domainData as $item) {
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
$domainsByProduct[(int) $item['product_id']] = $item['domains'];
}
}
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$product->is_type('licensed')) {
continue;
}
$productId = $product->get_id();
$domains = $domainsByProduct[$productId] ?? [];
// Get existing licenses for this product
$existingLicenses = $this->licenseManager->getLicensesByOrderAndProduct($orderId, $productId);
$existingDomains = array_map(fn($l) => $l->getDomain(), $existingLicenses);
foreach ($domains as $domain) {
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
// Skip if license already exists for this domain
if (in_array($normalizedDomain, $existingDomains, true)) {
$skipped++;
continue;
}
$license = $this->licenseManager->generateLicense(
$orderId,
$productId,
$customerId,
$normalizedDomain
);
if ($license) {
$generated++;
}
}
}
return ['generated' => $generated, 'skipped' => $skipped];
}
/**
* Generate licenses for legacy single domain format
*/
private function generateLegacyLicenses(\WC_Order $order, string $domain): array
{
$generated = 0;
$skipped = 0;
$orderId = $order->get_id();
$customerId = $order->get_customer_id();
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if (!$product || !$product->is_type('licensed')) {
continue;
}
// Check if license already exists
$existing = $this->licenseManager->getLicenseByOrderAndProduct($orderId, $product->get_id());
if ($existing) {
$skipped++;
continue;
}
$license = $this->licenseManager->generateLicense(
$orderId,
$product->get_id(),
$customerId,
$domain
);
if ($license) {
$generated++;
}
}
return ['generated' => $generated, 'skipped' => $skipped];
}
}

View File

@@ -202,6 +202,13 @@ final class SettingsController
'id' => 'wc_licensed_product_default_bind_to_version',
'default' => 'no',
],
'enable_multi_domain' => [
'name' => __('Enable Multi-Domain Licensing', 'wc-licensed-product'),
'type' => 'checkbox',
'desc' => __('Allow customers to purchase multiple licenses for different domains at once. Each unit in cart quantity requires a unique domain.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_enable_multi_domain',
'default' => 'no',
],
'section_end' => [
'type' => 'sectionend',
'id' => 'wc_licensed_product_section_defaults_end',
@@ -387,6 +394,14 @@ final class SettingsController
return get_option('wc_licensed_product_default_bind_to_version', 'no') === 'yes';
}
/**
* Check if multi-domain licensing is enabled
*/
public static function isMultiDomainEnabled(): bool
{
return get_option('wc_licensed_product_enable_multi_domain', 'no') === 'yes';
}
/**
* Check if expiration warning emails are enabled
* This checks both the WooCommerce email setting and the old setting for backwards compatibility