You've already forked wc-licensed-product
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:
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Jeremias\WcLicensedProduct\Checkout;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||
|
||||
/**
|
||||
* Handles checkout modifications for licensed products
|
||||
@@ -50,35 +51,75 @@ final class CheckoutController
|
||||
*/
|
||||
private function cartHasLicensedProducts(): bool
|
||||
{
|
||||
if (!WC()->cart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return !empty($this->getLicensedProductsFromCart());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add domain field to checkout form
|
||||
* Get licensed products from cart with quantities
|
||||
*
|
||||
* @return array<int, array{product_id: int, name: string, quantity: int}>
|
||||
*/
|
||||
private function getLicensedProductsFromCart(): array
|
||||
{
|
||||
if (!WC()->cart) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$licensedProducts = [];
|
||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||
$product = $cartItem['data'];
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
$productId = $product->get_id();
|
||||
$licensedProducts[$productId] = [
|
||||
'product_id' => $productId,
|
||||
'name' => $product->get_name(),
|
||||
'quantity' => (int) $cartItem['quantity'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $licensedProducts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add domain fields to checkout form
|
||||
* Shows multiple domain fields if multi-domain is enabled, otherwise single field
|
||||
*/
|
||||
public function addDomainField(): void
|
||||
{
|
||||
if (!$this->cartHasLicensedProducts()) {
|
||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||
if (empty($licensedProducts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if multi-domain licensing is enabled
|
||||
if (SettingsController::isMultiDomainEnabled()) {
|
||||
$this->renderMultiDomainFields($licensedProducts);
|
||||
} else {
|
||||
$this->renderSingleDomainField();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single domain field (legacy mode)
|
||||
*/
|
||||
private function renderSingleDomainField(): void
|
||||
{
|
||||
$savedValue = '';
|
||||
|
||||
// Check POST data first (validation failure case)
|
||||
if (isset($_POST['licensed_product_domain'])) {
|
||||
$savedValue = sanitize_text_field($_POST['licensed_product_domain']);
|
||||
} elseif (WC()->session) {
|
||||
$savedValue = WC()->session->get('licensed_product_domain', '');
|
||||
}
|
||||
|
||||
?>
|
||||
<div id="licensed-product-domain-field">
|
||||
<h3><?php esc_html_e('License Domain', 'wc-licensed-product'); ?></h3>
|
||||
<p class="form-row form-row-wide">
|
||||
<label for="licensed_product_domain">
|
||||
<?php esc_html_e('Domain for License Activation', 'wc-licensed-product'); ?>
|
||||
<?php esc_html_e('Domain', 'wc-licensed-product'); ?>
|
||||
<abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr>
|
||||
</label>
|
||||
<input
|
||||
@@ -87,10 +128,10 @@ final class CheckoutController
|
||||
name="licensed_product_domain"
|
||||
id="licensed_product_domain"
|
||||
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
|
||||
value="<?php echo esc_attr(WC()->checkout->get_value('licensed_product_domain')); ?>"
|
||||
value="<?php echo esc_attr($savedValue); ?>"
|
||||
/>
|
||||
<span class="description">
|
||||
<?php esc_html_e('Enter the domain where you will use this license (without http:// or www).', 'wc-licensed-product'); ?>
|
||||
<?php esc_html_e('Enter the domain where you will use the license (without http:// or www).', 'wc-licensed-product'); ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -98,62 +139,276 @@ final class CheckoutController
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain field during checkout
|
||||
* Render multi-domain fields (one per quantity)
|
||||
*/
|
||||
private function renderMultiDomainFields(array $licensedProducts): void
|
||||
{
|
||||
?>
|
||||
<div id="licensed-product-domain-fields">
|
||||
<h3><?php esc_html_e('License Domains', 'wc-licensed-product'); ?></h3>
|
||||
<p class="wclp-domain-description">
|
||||
<?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); ?>">
|
||||
<h4>
|
||||
<?php
|
||||
echo esc_html($productData['name']);
|
||||
if ($productData['quantity'] > 1) {
|
||||
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);
|
||||
?>
|
||||
<p class="form-row form-row-wide wclp-domain-row">
|
||||
<label for="<?php echo esc_attr($fieldId); ?>">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: license number */
|
||||
esc_html__('License %d:', 'wc-licensed-product'),
|
||||
$i + 1
|
||||
);
|
||||
?>
|
||||
<abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input-text wclp-domain-input"
|
||||
name="<?php echo esc_attr($fieldName); ?>"
|
||||
id="<?php echo esc_attr($fieldId); ?>"
|
||||
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
|
||||
value="<?php echo esc_attr($savedValue); ?>"
|
||||
/>
|
||||
</p>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<style>
|
||||
#licensed-product-domain-fields { margin-bottom: 20px; }
|
||||
#licensed-product-domain-fields h3 { margin-bottom: 10px; }
|
||||
.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-domain-row { margin-bottom: 10px; }
|
||||
.wclp-domain-row:last-child { margin-bottom: 0; }
|
||||
.wclp-domain-row label { display: block; margin-bottom: 5px; }
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved domain value from session/POST
|
||||
*/
|
||||
private function getSavedDomainValue(int $productId, int $index): string
|
||||
{
|
||||
// Check POST data first (validation failure case)
|
||||
if (isset($_POST['licensed_domains'][$productId][$index])) {
|
||||
return sanitize_text_field($_POST['licensed_domains'][$productId][$index]);
|
||||
}
|
||||
|
||||
// Check session for blocks checkout
|
||||
if (WC()->session) {
|
||||
$sessionDomains = WC()->session->get('licensed_product_domains', []);
|
||||
foreach ($sessionDomains as $item) {
|
||||
if (isset($item['product_id']) && (int) $item['product_id'] === $productId) {
|
||||
if (isset($item['domains'][$index])) {
|
||||
return $item['domains'][$index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain fields during checkout
|
||||
*/
|
||||
public function validateDomainField(): void
|
||||
{
|
||||
if (!$this->cartHasLicensedProducts()) {
|
||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||
if (empty($licensedProducts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = isset($_POST['licensed_product_domain'])
|
||||
? sanitize_text_field($_POST['licensed_product_domain'])
|
||||
: '';
|
||||
|
||||
if (empty($domain)) {
|
||||
wc_add_notice(
|
||||
__('Please enter a domain for your license activation.', 'wc-licensed-product'),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate domain format
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
if (!$this->isValidDomain($normalizedDomain)) {
|
||||
wc_add_notice(
|
||||
__('Please enter a valid domain name.', 'wc-licensed-product'),
|
||||
'error'
|
||||
);
|
||||
// Check if multi-domain licensing is enabled
|
||||
if (SettingsController::isMultiDomainEnabled()) {
|
||||
$this->validateMultiDomainFields($licensedProducts);
|
||||
} else {
|
||||
$this->validateSingleDomainField();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save domain field to order meta
|
||||
* Validate single domain field
|
||||
*/
|
||||
public function saveDomainField(int $orderId): void
|
||||
private function validateSingleDomainField(): void
|
||||
{
|
||||
if (!$this->cartHasLicensedProducts()) {
|
||||
$domain = isset($_POST['licensed_product_domain']) ? sanitize_text_field($_POST['licensed_product_domain']) : '';
|
||||
|
||||
if (empty($domain)) {
|
||||
wc_add_notice(__('Please enter a domain for your license.', 'wc-licensed-product'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($_POST['licensed_product_domain']) && !empty($_POST['licensed_product_domain'])) {
|
||||
$domain = sanitize_text_field($_POST['licensed_product_domain']);
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
if (!$this->isValidDomain($normalizedDomain)) {
|
||||
wc_add_notice(__('Please enter a valid domain for your license.', 'wc-licensed-product'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
$order = wc_get_order($orderId);
|
||||
if ($order) {
|
||||
$order->update_meta_data('_licensed_product_domain', $normalizedDomain);
|
||||
$order->save();
|
||||
/**
|
||||
* Validate multi-domain fields
|
||||
*/
|
||||
private function validateMultiDomainFields(array $licensedProducts): void
|
||||
{
|
||||
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
||||
|
||||
foreach ($licensedProducts as $productId => $productData) {
|
||||
$productDomains = $licensedDomains[$productId] ?? [];
|
||||
$normalizedDomains = [];
|
||||
|
||||
for ($i = 0; $i < $productData['quantity']; $i++) {
|
||||
$domain = isset($productDomains[$i]) ? sanitize_text_field($productDomains[$i]) : '';
|
||||
|
||||
// Check if domain is empty
|
||||
if (empty($domain)) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: 1: product name, 2: license number */
|
||||
__('Please enter a domain for %1$s (License %2$d).', 'wc-licensed-product'),
|
||||
$productData['name'],
|
||||
$i + 1
|
||||
),
|
||||
'error'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate domain format
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
if (!$this->isValidDomain($normalizedDomain)) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: 1: product name, 2: license number */
|
||||
__('Please enter a valid domain for %1$s (License %2$d).', 'wc-licensed-product'),
|
||||
$productData['name'],
|
||||
$i + 1
|
||||
),
|
||||
'error'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for duplicate domains within same product
|
||||
if (in_array($normalizedDomain, $normalizedDomains, true)) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: 1: domain name, 2: product name */
|
||||
__('The domain "%1$s" is used multiple times for %2$s. Each license requires a unique domain.', 'wc-licensed-product'),
|
||||
$normalizedDomain,
|
||||
$productData['name']
|
||||
),
|
||||
'error'
|
||||
);
|
||||
} else {
|
||||
$normalizedDomains[] = $normalizedDomain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domain in admin order view
|
||||
* Save domain fields to order meta
|
||||
*/
|
||||
public function saveDomainField(int $orderId): void
|
||||
{
|
||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||
if (empty($licensedProducts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = wc_get_order($orderId);
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if multi-domain licensing is enabled
|
||||
if (SettingsController::isMultiDomainEnabled()) {
|
||||
$this->saveMultiDomainFields($order, $licensedProducts);
|
||||
} else {
|
||||
$this->saveSingleDomainField($order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save single domain field to order meta (legacy format)
|
||||
*/
|
||||
private function saveSingleDomainField(\WC_Order $order): void
|
||||
{
|
||||
$domain = isset($_POST['licensed_product_domain']) ? sanitize_text_field($_POST['licensed_product_domain']) : '';
|
||||
|
||||
if (!empty($domain)) {
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
$order->update_meta_data('_licensed_product_domain', $normalizedDomain);
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save multi-domain fields to order meta
|
||||
*/
|
||||
private function saveMultiDomainFields(\WC_Order $order, array $licensedProducts): void
|
||||
{
|
||||
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
||||
$domainData = [];
|
||||
|
||||
foreach ($licensedProducts as $productId => $productData) {
|
||||
$productDomains = $licensedDomains[$productId] ?? [];
|
||||
$normalizedDomains = [];
|
||||
|
||||
for ($i = 0; $i < $productData['quantity']; $i++) {
|
||||
$domain = isset($productDomains[$i]) ? sanitize_text_field($productDomains[$i]) : '';
|
||||
if (!empty($domain)) {
|
||||
$normalizedDomains[] = $this->licenseManager->normalizeDomain($domain);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($normalizedDomains)) {
|
||||
$domainData[] = [
|
||||
'product_id' => $productId,
|
||||
'domains' => $normalizedDomains,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($domainData)) {
|
||||
$order->update_meta_data('_licensed_product_domains', $domainData);
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domains in admin order view
|
||||
*/
|
||||
public function displayDomainInAdmin(\WC_Order $order): void
|
||||
{
|
||||
// Try new multi-domain format first
|
||||
$domainData = $order->get_meta('_licensed_product_domains');
|
||||
if (!empty($domainData) && is_array($domainData)) {
|
||||
$this->displayMultiDomainsInAdmin($domainData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to legacy single domain
|
||||
$domain = $order->get_meta('_licensed_product_domain');
|
||||
if (!$domain) {
|
||||
return;
|
||||
@@ -168,10 +423,40 @@ final class CheckoutController
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domain in order emails
|
||||
* Display multi-domain data in admin
|
||||
*/
|
||||
private function displayMultiDomainsInAdmin(array $domainData): void
|
||||
{
|
||||
?>
|
||||
<div class="wclp-order-domains">
|
||||
<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');
|
||||
?>
|
||||
<p style="margin: 5px 0 5px 15px;">
|
||||
<em><?php echo esc_html($productName); ?>:</em><br>
|
||||
<?php echo esc_html(implode(', ', $item['domains'])); ?>
|
||||
</p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Display domains in order emails
|
||||
*/
|
||||
public function displayDomainInEmail(\WC_Order $order, bool $sentToAdmin, bool $plainText): void
|
||||
{
|
||||
// Try new multi-domain format first
|
||||
$domainData = $order->get_meta('_licensed_product_domains');
|
||||
if (!empty($domainData) && is_array($domainData)) {
|
||||
$this->displayMultiDomainsInEmail($domainData, $plainText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to legacy single domain
|
||||
$domain = $order->get_meta('_licensed_product_domain');
|
||||
if (!$domain) {
|
||||
return;
|
||||
@@ -189,6 +474,37 @@ final class CheckoutController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display multi-domain data in email
|
||||
*/
|
||||
private function displayMultiDomainsInEmail(array $domainData, bool $plainText): void
|
||||
{
|
||||
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');
|
||||
echo ' ' . esc_html($productName) . ': ' . esc_html(implode(', ', $item['domains'])) . "\n";
|
||||
}
|
||||
} else {
|
||||
?>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<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');
|
||||
?>
|
||||
<p style="margin: 5px 0 5px 15px;">
|
||||
<em><?php echo esc_html($productName); ?>:</em><br>
|
||||
<?php echo esc_html(implode(', ', $item['domains'])); ?>
|
||||
</p>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain format
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user