/** * 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 = `
${settings.fieldDescription || 'Enter a unique domain for each license.'}
${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 `${settings.fieldDescription || 'Enter the domain where you will use the license.'}