Files
wc-licensed-product/assets/js/checkout-blocks.js
magdev d29697ac62 Fix licenses not showing in admin order form for variable products (v0.5.13)
- Fix OrderLicenseController to use isLicensedProduct() for consistent product type detection
- Fixed expected licenses calculation for variable product orders
- Fixed manual license generation from admin order page for variable products
- Remove debug logging from all source files (PHP and JavaScript)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 15:45:32 +01:00

502 lines
20 KiB
JavaScript
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.
/**
* WooCommerce Checkout Blocks Integration
*
* Adds domain fields to the checkout block for licensed products.
* Supports single domain mode (legacy) and multi-domain mode (per quantity).
*
* @package WcLicensedProduct
*/
(function () {
'use strict';
// Check dependencies
if (typeof wc === 'undefined' ||
typeof wc.blocksCheckout === 'undefined' ||
typeof wc.wcSettings === 'undefined') {
return;
}
const { getSetting } = wc.wcSettings;
const { createElement, useState, useEffect, useCallback } = wp.element;
const { TextControl } = wp.components;
const { __ } = wp.i18n;
// Get available exports from blocksCheckout
const { ExperimentalOrderMeta, extensionCartUpdate } = wc.blocksCheckout;
// Debounce function for API updates
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Get settings from PHP
const settings = getSetting('wc-licensed-product_data', {});
// Check if we have licensed products
if (!settings.hasLicensedProducts) {
return;
}
/**
* Validate domain format
*/
function isValidDomain(domain) {
if (!domain || domain.length > 255) return false;
const pattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
return pattern.test(domain);
}
/**
* Normalize domain
*/
function normalizeDomain(domain) {
return domain.toLowerCase().trim()
.replace(/^https?:\/\//, '')
.replace(/^www\./, '')
.replace(/\/.*$/, '');
}
/**
* Single Domain Component
*/
const SingleDomainField = () => {
const [domain, setDomain] = useState('');
const [error, setError] = useState('');
// Debounced API update function
const updateStoreApi = useCallback(
debounce((normalizedDomain) => {
if (extensionCartUpdate) {
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domain: normalizedDomain,
},
}).catch(err => {
console.error('[WCLP] Store API update error:', err);
});
}
}, 500),
[]
);
const handleChange = (value) => {
const normalized = normalizeDomain(value);
setDomain(normalized);
if (normalized && !isValidDomain(normalized)) {
setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'));
} else {
setError('');
// Update Store API when valid
updateStoreApi(normalized);
}
// Store in hidden input for form submission (fallback)
const hiddenInput = document.getElementById('wclp-domain-hidden');
if (hiddenInput) {
hiddenInput.value = normalized;
}
};
return createElement(
'div',
{
className: 'wc-block-components-licensed-product-domain',
style: {
padding: '16px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
marginBottom: '16px',
}
},
createElement('h4', { style: { marginTop: 0, marginBottom: '8px' } },
settings.sectionTitle || __('License Domain', 'wc-licensed-product')
),
createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } },
settings.fieldDescription || __('Enter the domain where you will use the license.', 'wc-licensed-product')
),
createElement(TextControl, {
label: settings.singleDomainLabel || __('Domain', 'wc-licensed-product'),
value: domain,
onChange: handleChange,
placeholder: settings.fieldPlaceholder || 'example.com',
help: error || '',
className: error ? 'has-error' : '',
}),
createElement('input', {
type: 'hidden',
id: 'wclp-domain-hidden',
name: 'wclp_license_domain',
value: domain,
})
);
};
/**
* Get unique key for product (handles variations)
*/
function getProductKey(product) {
if (product.variation_id && product.variation_id > 0) {
return `${product.product_id}_${product.variation_id}`;
}
return String(product.product_id);
}
/**
* Multi-Domain Component
*/
const MultiDomainFields = () => {
const products = settings.licensedProducts || [];
const [domains, setDomains] = useState(() => {
const init = {};
products.forEach(p => {
const key = getProductKey(p);
init[key] = Array(p.quantity).fill('');
});
return init;
});
const [errors, setErrors] = useState({});
// Debounced API update function
const updateStoreApi = useCallback(
debounce((domainsData) => {
if (extensionCartUpdate) {
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domains: domainsData,
},
}).catch(err => {
console.error('[WCLP] Store API update error:', err);
});
}
}, 500),
[]
);
if (!products.length) {
return null;
}
const handleChange = (productKey, index, value) => {
const normalized = normalizeDomain(value);
const newDomains = { ...domains };
if (!newDomains[productKey]) newDomains[productKey] = [];
newDomains[productKey] = [...newDomains[productKey]];
newDomains[productKey][index] = normalized;
setDomains(newDomains);
// Validate
const key = `${productKey}_${index}`;
const newErrors = { ...errors };
if (normalized && !isValidDomain(normalized)) {
newErrors[key] = settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product');
} else {
delete newErrors[key];
}
// Check for duplicates within same product/variation
const productDomains = newDomains[productKey].filter(d => d);
const uniqueDomains = new Set(productDomains.map(d => normalizeDomain(d)));
if (productDomains.length !== uniqueDomains.size) {
const seen = new Set();
newDomains[productKey].forEach((d, idx) => {
const normalizedD = normalizeDomain(d);
const dupKey = `${productKey}_${idx}`;
if (normalizedD && seen.has(normalizedD)) {
newErrors[dupKey] = settings.duplicateError || __('Each license requires a unique domain.', 'wc-licensed-product');
} else if (normalizedD) {
seen.add(normalizedD);
}
});
}
setErrors(newErrors);
// Build domain data for Store API
const data = products.map(p => {
const pKey = getProductKey(p);
const doms = newDomains[pKey] || [];
const entry = {
product_id: p.product_id,
domains: doms.filter(d => d),
};
if (p.variation_id && p.variation_id > 0) {
entry.variation_id = p.variation_id;
}
return entry;
}).filter(item => item.domains.length > 0);
// Update Store API
updateStoreApi(data);
// Update hidden field (fallback)
const hiddenInput = document.getElementById('wclp-domains-hidden');
if (hiddenInput) {
hiddenInput.value = JSON.stringify(data);
}
};
return createElement(
'div',
{
className: 'wc-block-components-licensed-product-domains',
style: {
padding: '16px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
marginBottom: '16px',
}
},
createElement('h4', { style: { marginTop: 0, marginBottom: '8px' } },
settings.sectionTitle || __('License Domains', 'wc-licensed-product')
),
createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } },
settings.fieldDescription || __('Enter a unique domain for each license.', 'wc-licensed-product')
),
products.map(product => {
const productKey = getProductKey(product);
const durationLabel = product.duration_label || '';
const displayName = durationLabel
? `${product.name} (${durationLabel})`
: product.name;
return createElement(
'div',
{
key: productKey,
style: {
marginBottom: '16px',
padding: '12px',
backgroundColor: '#fff',
borderRadius: '4px',
}
},
createElement('strong', { style: { display: 'block', marginBottom: '8px' } },
displayName + (product.quantity > 1 ? ` ×${product.quantity}` : '')
),
Array.from({ length: product.quantity }, (_, i) => {
const key = `${productKey}_${i}`;
return createElement(
'div',
{ key: i, style: { marginBottom: '8px' } },
createElement(TextControl, {
label: (settings.licenseLabel || __('License %d:', 'wc-licensed-product')).replace('%d', i + 1),
value: domains[productKey]?.[i] || '',
onChange: (val) => handleChange(productKey, i, val),
placeholder: settings.fieldPlaceholder || 'example.com',
help: errors[key] || '',
})
);
})
);
}),
createElement('input', {
type: 'hidden',
id: 'wclp-domains-hidden',
name: 'wclp_license_domains',
value: '',
})
);
};
/**
* Main License Domains Block
*/
const LicenseDomainsBlock = () => {
if (settings.isMultiDomainEnabled) {
return createElement(MultiDomainFields);
}
return createElement(SingleDomainField);
};
// Register using ExperimentalOrderMeta slot
if (ExperimentalOrderMeta) {
const { registerPlugin } = wp.plugins || {};
if (registerPlugin) {
registerPlugin('wc-licensed-product-domain-fields', {
render: () => {
return createElement(
ExperimentalOrderMeta,
{},
createElement(LicenseDomainsBlock)
);
},
scope: 'woocommerce-checkout',
});
}
}
// Fallback: inject into DOM directly if React approach fails
setTimeout(function() {
const existingComponent = document.querySelector('.wc-block-components-licensed-product-domain, .wc-block-components-licensed-product-domains');
if (existingComponent) {
return;
}
const checkoutForm = document.querySelector('.wc-block-checkout, .wc-block-checkout__form, form.checkout');
if (!checkoutForm) {
return;
}
const contactInfo = document.querySelector('.wc-block-checkout__contact-fields, .wp-block-woocommerce-checkout-contact-information-block');
const paymentMethods = document.querySelector('.wc-block-checkout__payment-method, .wp-block-woocommerce-checkout-payment-block');
let insertionPoint = contactInfo || paymentMethods;
if (!insertionPoint) {
insertionPoint = checkoutForm.querySelector('.wc-block-components-form');
}
if (!insertionPoint) {
return;
}
const container = document.createElement('div');
container.id = 'wclp-domain-fields-container';
container.className = 'wc-block-components-licensed-product-wrapper';
container.style.cssText = 'margin: 20px 0; padding: 16px; background: #f0f0f0; border-radius: 4px;';
if (settings.isMultiDomainEnabled && settings.licensedProducts) {
container.innerHTML = `
<h4 style="margin: 0 0 8px 0;">${settings.sectionTitle || 'License Domains'}</h4>
<p style="margin-bottom: 12px; color: #666; font-size: 0.9em;">
${settings.fieldDescription || 'Enter a unique domain for each license.'}
</p>
${settings.licensedProducts.map(product => {
const productKey = product.variation_id && product.variation_id > 0
? `${product.product_id}_${product.variation_id}`
: product.product_id;
const durationLabel = product.duration_label || '';
const displayName = durationLabel
? `${product.name} (${durationLabel})`
: product.name;
return `
<div style="margin-bottom: 16px; padding: 12px; background: #fff; border-radius: 4px;">
<strong style="display: block; margin-bottom: 8px;">
${displayName}${product.quantity > 1 ? ` ×${product.quantity}` : ''}
</strong>
${Array.from({ length: product.quantity }, (_, i) => `
<div style="margin-bottom: 8px;">
<label style="display: block; margin-bottom: 4px;">
${(settings.licenseLabel || 'License %d:').replace('%d', i + 1)}
</label>
<input type="text"
name="licensed_domains[${productKey}][${i}]"
placeholder="${settings.fieldPlaceholder || 'example.com'}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
/>
${product.variation_id && product.variation_id > 0 ? `
<input type="hidden"
name="licensed_variation_ids[${productKey}]"
value="${product.variation_id}"
/>
` : ''}
</div>
`).join('')}
</div>
`}).join('')}
`;
} else {
container.innerHTML = `
<h4 style="margin: 0 0 8px 0;">${settings.sectionTitle || 'License Domain'}</h4>
<p style="margin-bottom: 12px; color: #666; font-size: 0.9em;">
${settings.fieldDescription || 'Enter the domain where you will use the license.'}
</p>
<div style="margin-bottom: 8px;">
<label style="display: block; margin-bottom: 4px;">
${settings.singleDomainLabel || 'Domain'}
</label>
<input type="text"
name="licensed_product_domain"
placeholder="${settings.fieldPlaceholder || 'example.com'}"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
/>
</div>
`;
}
if (contactInfo) {
contactInfo.parentNode.insertBefore(container, contactInfo.nextSibling);
} else if (paymentMethods) {
paymentMethods.parentNode.insertBefore(container, paymentMethods);
} else {
insertionPoint.appendChild(container);
}
// Add event listeners to sync with Store API
const debouncedUpdate = debounce(function() {
if (!extensionCartUpdate) {
return;
}
if (settings.isMultiDomainEnabled && settings.licensedProducts) {
// Collect multi-domain data
const domainsData = settings.licensedProducts.map(function(product) {
const productKey = product.variation_id && product.variation_id > 0
? product.product_id + '_' + product.variation_id
: String(product.product_id);
const domains = [];
for (let i = 0; i < product.quantity; i++) {
const input = container.querySelector('input[name="licensed_domains[' + productKey + '][' + i + ']"]');
if (input && input.value.trim()) {
domains.push(normalizeDomain(input.value));
}
}
const entry = {
product_id: product.product_id,
domains: domains,
};
if (product.variation_id && product.variation_id > 0) {
entry.variation_id = product.variation_id;
}
return entry;
}).filter(function(item) { return item.domains.length > 0; });
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domains: domainsData,
},
}).catch(function(err) {
console.error('[WCLP] Store API update error:', err);
});
} else {
// Single domain
const input = container.querySelector('input[name="licensed_product_domain"]');
if (input) {
const domain = normalizeDomain(input.value);
extensionCartUpdate({
namespace: 'wc-licensed-product',
data: {
licensed_product_domain: domain,
},
}).catch(function(err) {
console.error('[WCLP] Store API update error:', err);
});
}
}
}, 500);
// Attach event listeners to all domain inputs
container.querySelectorAll('input[type="text"]').forEach(function(input) {
input.addEventListener('input', debouncedUpdate);
input.addEventListener('change', debouncedUpdate);
});
}, 2000);
})();