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

@@ -12,6 +12,7 @@ namespace Jeremias\WcLicensedProduct\Checkout;
use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema;
use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use Jeremias\WcLicensedProduct\License\LicenseManager;
/**
@@ -70,6 +71,12 @@ final class StoreApiExtension
*/
public function getExtensionData(): array
{
if (SettingsController::isMultiDomainEnabled()) {
return [
'licensed_product_domains' => WC()->session ? WC()->session->get('licensed_product_domains', []) : [],
];
}
return [
'licensed_product_domain' => WC()->session ? WC()->session->get('licensed_product_domain', '') : '',
];
@@ -80,6 +87,31 @@ final class StoreApiExtension
*/
public function getExtensionSchema(): array
{
if (SettingsController::isMultiDomainEnabled()) {
return [
'licensed_product_domains' => [
'description' => __('Domains for license activation by product', 'wc-licensed-product'),
'type' => 'array',
'context' => ['view', 'edit'],
'readonly' => false,
'items' => [
'type' => 'object',
'properties' => [
'product_id' => [
'type' => 'integer',
],
'domains' => [
'type' => 'array',
'items' => [
'type' => 'string',
],
],
],
],
],
];
}
return [
'licensed_product_domain' => [
'description' => __('Domain for license activation', 'wc-licensed-product'),
@@ -95,30 +127,103 @@ final class StoreApiExtension
*/
public function handleExtensionUpdate(array $data): void
{
if (isset($data['licensed_product_domain'])) {
$domain = sanitize_text_field($data['licensed_product_domain']);
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
if (SettingsController::isMultiDomainEnabled()) {
// Multi-domain mode
if (isset($data['licensed_product_domains']) && is_array($data['licensed_product_domains'])) {
$normalizedData = $this->normalizeDomainsData($data['licensed_product_domains']);
if (WC()->session) {
WC()->session->set('licensed_product_domain', $normalizedDomain);
if (WC()->session) {
WC()->session->set('licensed_product_domains', $normalizedData);
}
}
} else {
// Single domain mode
if (isset($data['licensed_product_domain'])) {
$sanitized = sanitize_text_field($data['licensed_product_domain']);
$normalized = $this->licenseManager->normalizeDomain($sanitized);
if (WC()->session) {
WC()->session->set('licensed_product_domain', $normalized);
}
}
}
}
/**
* Process the checkout order - save domain to order meta
* Normalize domains data from frontend
*/
private function normalizeDomainsData(array $domainsData): array
{
$normalized = [];
foreach ($domainsData as $item) {
if (!isset($item['product_id']) || !isset($item['domains']) || !is_array($item['domains'])) {
continue;
}
$productId = (int) $item['product_id'];
$domains = [];
foreach ($item['domains'] as $domain) {
$sanitized = sanitize_text_field($domain);
if (!empty($sanitized)) {
$domains[] = $this->licenseManager->normalizeDomain($sanitized);
}
}
if (!empty($domains)) {
$normalized[] = [
'product_id' => $productId,
'domains' => $domains,
];
}
}
return $normalized;
}
/**
* Process the checkout order - save domains to order meta
*/
public function processCheckoutOrder(\WC_Order $order): void
{
$domain = WC()->session ? WC()->session->get('licensed_product_domain', '') : '';
$requestData = json_decode(file_get_contents('php://input'), true);
// Also check in the request data for block checkout
if (empty($domain)) {
$requestData = json_decode(file_get_contents('php://input'), true);
if (isset($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain'])) {
$domain = sanitize_text_field($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain']);
$domain = $this->licenseManager->normalizeDomain($domain);
}
if (SettingsController::isMultiDomainEnabled()) {
$this->processMultiDomainOrder($order, $requestData);
} else {
$this->processSingleDomainOrder($order, $requestData);
}
}
/**
* Process order in single domain mode (legacy)
*/
private function processSingleDomainOrder(\WC_Order $order, ?array $requestData): void
{
$domain = '';
// Check session first
if (WC()->session) {
$domain = WC()->session->get('licensed_product_domain', '');
}
// Check in the request data for block checkout (extension data)
if (empty($domain) && isset($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain'])) {
$sanitized = sanitize_text_field($requestData['extensions'][self::IDENTIFIER]['licensed_product_domain']);
$domain = $this->licenseManager->normalizeDomain($sanitized);
}
// Check for wclp_license_domain (from our hidden input)
if (empty($domain) && isset($requestData['wclp_license_domain'])) {
$sanitized = sanitize_text_field($requestData['wclp_license_domain']);
$domain = $this->licenseManager->normalizeDomain($sanitized);
}
// Check for additional_fields (WC Blocks API)
if (empty($domain) && isset($requestData['additional_fields']['wc-licensed-product/domain'])) {
$sanitized = sanitize_text_field($requestData['additional_fields']['wc-licensed-product/domain']);
$domain = $this->licenseManager->normalizeDomain($sanitized);
}
if (!empty($domain)) {
@@ -131,4 +236,65 @@ final class StoreApiExtension
}
}
}
/**
* Process order in multi-domain mode
*/
private function processMultiDomainOrder(\WC_Order $order, ?array $requestData): void
{
$domainData = [];
// Check session first
if (WC()->session) {
$domainData = WC()->session->get('licensed_product_domains', []);
}
// Check in the request data for block checkout (extension data)
if (empty($domainData) && isset($requestData['extensions'][self::IDENTIFIER]['licensed_product_domains'])) {
$domainData = $this->normalizeDomainsData(
$requestData['extensions'][self::IDENTIFIER]['licensed_product_domains']
);
}
// Check for wclp_license_domains (from our hidden input - JSON string)
if (empty($domainData) && isset($requestData['wclp_license_domains'])) {
$parsed = json_decode($requestData['wclp_license_domains'], true);
if (is_array($parsed)) {
$domainData = $this->normalizeDomainsData($parsed);
}
}
// 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) {
if (!is_array($domains)) {
continue;
}
$normalizedDomains = [];
foreach ($domains as $domain) {
$sanitized = sanitize_text_field($domain);
if (!empty($sanitized)) {
$normalizedDomains[] = $this->licenseManager->normalizeDomain($sanitized);
}
}
if (!empty($normalizedDomains)) {
$domainData[] = [
'product_id' => (int) $productId,
'domains' => $normalizedDomains,
];
}
}
}
if (!empty($domainData)) {
$order->update_meta_data('_licensed_product_domains', $domainData);
$order->save();
// Clear session data
if (WC()->session) {
WC()->session->set('licensed_product_domains', []);
}
}
}
}