Files
wc-licensed-product/src/Checkout/CheckoutController.php
magdev c31df1e8c4 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>
2026-01-26 16:14:15 +01:00

642 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Checkout Controller
*
* @package Jeremias\WcLicensedProduct\Checkout
*/
declare(strict_types=1);
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
*/
final class CheckoutController
{
private LicenseManager $licenseManager;
public function __construct(LicenseManager $licenseManager)
{
$this->licenseManager = $licenseManager;
$this->registerHooks();
}
/**
* Register WordPress hooks
*/
private function registerHooks(): void
{
// Add domain field to checkout
add_action('woocommerce_after_order_notes', [$this, 'addDomainField']);
// Validate domain field
add_action('woocommerce_checkout_process', [$this, 'validateDomainField']);
// Save domain field to order meta
add_action('woocommerce_checkout_update_order_meta', [$this, 'saveDomainField']);
// Display domain in order details (admin)
add_action('woocommerce_admin_order_data_after_billing_address', [$this, 'displayDomainInAdmin']);
// Display domain in order email
add_action('woocommerce_email_after_order_table', [$this, 'displayDomainInEmail'], 10, 3);
}
/**
* Check if cart contains licensed products
*/
private function cartHasLicensedProducts(): bool
{
return !empty($this->getLicensedProductsFromCart());
}
/**
* Get licensed products from cart with quantities
*
* @return array<string, array{product_id: int, variation_id: int, name: string, quantity: int, duration_label: string}>
*/
private function getLicensedProductsFromCart(): array
{
if (!WC()->cart) {
return [];
}
$licensedProducts = [];
foreach (WC()->cart->get_cart() as $cartItem) {
$product = $cartItem['data'];
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,
];
}
}
}
return $licensedProducts;
}
/**
* Add domain fields to checkout form
* Shows multiple domain fields if multi-domain is enabled, otherwise single field
*/
public function addDomainField(): void
{
$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', 'wc-licensed-product'); ?>
<abbr class="required" title="<?php esc_attr_e('required', 'wc-licensed-product'); ?>">*</abbr>
</label>
<input
type="text"
class="input-text"
name="licensed_product_domain"
id="licensed_product_domain"
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
value="<?php echo esc_attr($savedValue); ?>"
/>
<span class="description">
<?php esc_html_e('Enter the domain where you will use the license (without http:// or www).', 'wc-licensed-product'); ?>
</span>
</p>
</div>
<?php
}
/**
* 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 $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']);
}
?>
</h4>
<?php for ($i = 0; $i < $productData['quantity']; $i++): ?>
<?php
$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); ?>">
<?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); ?>"
/>
<?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>
<?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-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; }
</style>
<?php
}
/**
* Get saved domain value from session/POST
*/
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]);
}
// Check session for blocks checkout
if (WC()->session) {
$sessionDomains = WC()->session->get('licensed_product_domains', []);
foreach ($sessionDomains as $item) {
$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];
}
}
}
}
return '';
}
/**
* Validate domain fields during checkout
*/
public function validateDomainField(): void
{
$licensedProducts = $this->getLicensedProductsFromCart();
if (empty($licensedProducts)) {
return;
}
// Check if multi-domain licensing is enabled
if (SettingsController::isMultiDomainEnabled()) {
$this->validateMultiDomainFields($licensedProducts);
} else {
$this->validateSingleDomainField();
}
}
/**
* Validate single domain field
*/
private function validateSingleDomainField(): void
{
$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;
}
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
if (!$this->isValidDomain($normalizedDomain)) {
wc_add_notice(__('Please enter a valid domain for your license.', 'wc-licensed-product'), 'error');
}
}
/**
* Validate multi-domain fields
*/
private function validateMultiDomainFields(array $licensedProducts): void
{
$licensedDomains = $_POST['licensed_domains'] ?? [];
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++) {
$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/variation
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;
}
}
}
}
/**
* 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'] ?? [];
$licensedVariationIds = $_POST['licensed_variation_ids'] ?? [];
$domainData = [];
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++) {
$domain = isset($productDomains[$i]) ? sanitize_text_field($productDomains[$i]) : '';
if (!empty($domain)) {
$normalizedDomains[] = $this->licenseManager->normalizeDomain($domain);
}
}
if (!empty($normalizedDomains)) {
$entry = [
'product_id' => $productId,
'domains' => $normalizedDomains,
];
// Include variation_id if present
if ($variationId > 0) {
$entry['variation_id'] = $variationId;
}
$domainData[] = $entry;
}
}
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;
}
?>
<p>
<strong><?php esc_html_e('License Domain:', 'wc-licensed-product'); ?></strong>
<?php echo esc_html($domain); ?>
</p>
<?php
}
/**
* 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
$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>
<?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;
}
if ($plainText) {
echo "\n" . esc_html__('License Domain:', 'wc-licensed-product') . ' ' . esc_html($domain) . "\n";
} else {
?>
<p>
<strong><?php esc_html_e('License Domain:', 'wc-licensed-product'); ?></strong>
<?php echo esc_html($domain); ?>
</p>
<?php
}
}
/**
* 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) {
$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 {
?>
<div style="margin-bottom: 15px;">
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
<?php foreach ($domainData as $item): ?>
<?php
$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>
<?php echo esc_html(implode(', ', $item['domains'])); ?>
</p>
<?php endforeach; ?>
</div>
<?php
}
}
/**
* Validate domain format
*/
private function isValidDomain(string $domain): bool
{
// Basic domain validation
if (strlen($domain) > 255) {
return false;
}
// Check for valid domain pattern
$pattern = '/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/';
return (bool) preg_match($pattern, $domain);
}
}