Files
wc-licensed-product/assets/js/checkout-blocks.js

385 lines
15 KiB
JavaScript
Raw Normal View History

/**
* 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 } = wp.element;
const { TextControl } = wp.components;
const { __ } = wp.i18n;
// Get available exports from blocksCheckout
const { ExperimentalOrderMeta } = wc.blocksCheckout;
// 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('');
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('');
}
// Store in hidden input for form submission
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({});
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);
// 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) {
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: () => 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);
}
}, 2000);
})();