You've already forked wc-licensed-product
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>
This commit is contained in:
@@ -110,6 +110,16 @@
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -118,7 +128,8 @@
|
||||
const [domains, setDomains] = useState(() => {
|
||||
const init = {};
|
||||
products.forEach(p => {
|
||||
init[p.product_id] = Array(p.quantity).fill('');
|
||||
const key = getProductKey(p);
|
||||
init[key] = Array(p.quantity).fill('');
|
||||
});
|
||||
return init;
|
||||
});
|
||||
@@ -128,16 +139,16 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleChange = (productId, index, value) => {
|
||||
const handleChange = (productKey, index, value) => {
|
||||
const normalized = normalizeDomain(value);
|
||||
const newDomains = { ...domains };
|
||||
if (!newDomains[productId]) newDomains[productId] = [];
|
||||
newDomains[productId] = [...newDomains[productId]];
|
||||
newDomains[productId][index] = normalized;
|
||||
if (!newDomains[productKey]) newDomains[productKey] = [];
|
||||
newDomains[productKey] = [...newDomains[productKey]];
|
||||
newDomains[productKey][index] = normalized;
|
||||
setDomains(newDomains);
|
||||
|
||||
// Validate
|
||||
const key = `${productId}_${index}`;
|
||||
const key = `${productKey}_${index}`;
|
||||
const newErrors = { ...errors };
|
||||
if (normalized && !isValidDomain(normalized)) {
|
||||
newErrors[key] = settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product');
|
||||
@@ -145,14 +156,14 @@
|
||||
delete newErrors[key];
|
||||
}
|
||||
|
||||
// Check for duplicates within same product
|
||||
const productDomains = newDomains[productId].filter(d => d);
|
||||
// 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[productId].forEach((d, idx) => {
|
||||
newDomains[productKey].forEach((d, idx) => {
|
||||
const normalizedD = normalizeDomain(d);
|
||||
const dupKey = `${productId}_${idx}`;
|
||||
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) {
|
||||
@@ -163,11 +174,19 @@
|
||||
|
||||
setErrors(newErrors);
|
||||
|
||||
// Update hidden field
|
||||
const data = Object.entries(newDomains).map(([pid, doms]) => ({
|
||||
product_id: parseInt(pid, 10),
|
||||
domains: doms.filter(d => d),
|
||||
})).filter(item => item.domains.length > 0);
|
||||
// Update hidden field with variation support
|
||||
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);
|
||||
|
||||
const hiddenInput = document.getElementById('wclp-domains-hidden');
|
||||
if (hiddenInput) {
|
||||
@@ -192,35 +211,43 @@
|
||||
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 => createElement(
|
||||
'div',
|
||||
{
|
||||
key: product.product_id,
|
||||
style: {
|
||||
marginBottom: '16px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '4px',
|
||||
}
|
||||
},
|
||||
createElement('strong', { style: { display: 'block', marginBottom: '8px' } },
|
||||
product.name + (product.quantity > 1 ? ` (×${product.quantity})` : '')
|
||||
),
|
||||
Array.from({ length: product.quantity }, (_, i) => {
|
||||
const key = `${product.product_id}_${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[product.product_id]?.[i] || '',
|
||||
onChange: (val) => handleChange(product.product_id, i, val),
|
||||
placeholder: settings.fieldPlaceholder || 'example.com',
|
||||
help: errors[key] || '',
|
||||
})
|
||||
);
|
||||
})
|
||||
)),
|
||||
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',
|
||||
@@ -291,10 +318,19 @@
|
||||
<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 => `
|
||||
${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;">
|
||||
${product.name}${product.quantity > 1 ? ` (×${product.quantity})` : ''}
|
||||
${displayName}${product.quantity > 1 ? ` ×${product.quantity}` : ''}
|
||||
</strong>
|
||||
${Array.from({ length: product.quantity }, (_, i) => `
|
||||
<div style="margin-bottom: 8px;">
|
||||
@@ -302,14 +338,20 @@
|
||||
${(settings.licenseLabel || 'License %d:').replace('%d', i + 1)}
|
||||
</label>
|
||||
<input type="text"
|
||||
name="licensed_domains[${product.product_id}][${i}]"
|
||||
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('')}
|
||||
`}).join('')}
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
|
||||
Reference in New Issue
Block a user