You've already forked wc-licensed-product
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c31df1e8c4 | |||
| 8cac742f57 | |||
| 41e46fc7b8 | |||
| 549a58dc5d | |||
| 7d02105284 |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.3] - 2026-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Variable licensed product type (`licensed-variable`) for selling licenses with different durations
|
||||||
|
- Support for monthly, yearly, quarterly, or lifetime license variations
|
||||||
|
- `LicensedVariableProduct` class extending `WC_Product_Variable`
|
||||||
|
- `LicensedProductVariation` class for individual variation license settings
|
||||||
|
- Variation-specific license duration settings in product edit page
|
||||||
|
- Duration labels displayed in checkout domain fields (e.g., "Yearly License")
|
||||||
|
- Variation ID tracking in order domain meta for proper license generation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `LicenseManager::generateLicense()` to accept optional variation ID
|
||||||
|
- Checkout now handles variations with separate domain fields per product/variation
|
||||||
|
- WooCommerce Blocks checkout updated to display variation duration labels
|
||||||
|
- Store API extension updated to include variation_id in domain data schema
|
||||||
|
|
||||||
|
## [0.5.2] - 2026-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Per-license customer secrets for API response verification
|
||||||
|
- "API Verification Secret" section in customer account licenses page (collapsible)
|
||||||
|
- Copy button for customer secrets with clipboard support
|
||||||
|
- Documentation for per-license secret derivation and usage
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Customers no longer need the master server secret for signature verification
|
||||||
|
- Each license key has a unique derived secret using HKDF-like key derivation
|
||||||
|
- If one customer's secret is compromised, other customers remain unaffected
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `ResponseSigner` with static methods for secret derivation
|
||||||
|
- Updated `server-implementation.md` with per-license secret documentation
|
||||||
|
- Added new translation strings for secret-related UI
|
||||||
|
|
||||||
## [0.5.1] - 2026-01-26
|
## [0.5.1] - 2026-01-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
126
CLAUDE.md
126
CLAUDE.md
@@ -32,18 +32,10 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||||
|
|
||||||
### Known Bugs
|
|
||||||
|
|
||||||
No known bugs at the moment.
|
|
||||||
|
|
||||||
### Version 0.6.0
|
### Version 0.6.0
|
||||||
|
|
||||||
*No planned features yet.*
|
*No planned features yet.*
|
||||||
|
|
||||||
### Version 0.5.1
|
|
||||||
|
|
||||||
*No planned bugfixes yet.*
|
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **Language:** PHP 8.3.x
|
||||||
@@ -1268,3 +1260,121 @@ Major feature release enabling customers to purchase multiple licenses for diffe
|
|||||||
- Created release package: `releases/wc-licensed-product-0.5.0.zip` (863 KB)
|
- Created release package: `releases/wc-licensed-product-0.5.0.zip` (863 KB)
|
||||||
- SHA256: `446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316`
|
- SHA256: `446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316`
|
||||||
- Tagged as `v0.5.0` and pushed to `main` branch
|
- Tagged as `v0.5.0` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-26 - Version 0.5.1 - Admin UI Fixes
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Bug fix release improving admin UI usability for version management and license overview.
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- Fixed: Product versions in admin now sort by version DESC when adding via AJAX
|
||||||
|
- Fixed: License actions in admin overview are now always visible (not just on hover)
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `assets/css/admin.css` - Added `!important` to `.licenses-table .row-actions` for permanent visibility
|
||||||
|
- `assets/js/versions.js` - Added `compareVersions()` function and sorted insertion for AJAX-added versions
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Version sorting uses semantic version comparison (major.minor.patch)
|
||||||
|
- New versions are inserted in correct sorted position in the table instead of always appending
|
||||||
|
- CSS override uses `!important` to overcome WordPress default hover-only behavior for row actions
|
||||||
|
- `compareVersions()` function compares version strings numerically (1.10.0 > 1.9.0)
|
||||||
|
|
||||||
|
**Release v0.5.1:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.5.1.zip` (863 KB)
|
||||||
|
- SHA256: `a489f0b8cfcd7d5d9b2021b7ff581b9f1a56468dfde87bbb06bb4555d11f7556`
|
||||||
|
- Tagged as `v0.5.1` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-26 - Version 0.5.2 - Per-License Customer Secrets
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Security enhancement release adding per-license customer secrets for API response verification. Each customer now receives a unique secret derived from their license key, eliminating the need to share a global server secret.
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Per-license secret derivation using HKDF-like approach
|
||||||
|
- Customer account UI showing API verification secret with collapsible section
|
||||||
|
- Copy-to-clipboard functionality for customer secrets
|
||||||
|
- Static helper methods in ResponseSigner for secret derivation
|
||||||
|
|
||||||
|
**New methods in ResponseSigner:**
|
||||||
|
|
||||||
|
- `deriveCustomerSecret()` - Static method to derive customer secret from license key and server secret
|
||||||
|
- `getCustomerSecretForLicense()` - Static method to get customer secret using configured server secret
|
||||||
|
- `isSigningEnabled()` - Static method to check if response signing is configured
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Api/ResponseSigner.php` - Added static methods for customer secret derivation
|
||||||
|
- `src/Frontend/AccountController.php` - Added `signing_enabled` and `customer_secret` to template data
|
||||||
|
- `templates/frontend/licenses.html.twig` - Added collapsible secret section with toggle and copy button
|
||||||
|
- `assets/css/frontend.css` - Added styles for `.license-row-secret`, `.secret-toggle`, `.secret-content`
|
||||||
|
- `assets/js/frontend.js` - Added `toggleSecret()` and `copySecret()` event handlers
|
||||||
|
- `docs/server-implementation.md` - Added documentation for per-license secrets
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Secret derivation uses HKDF-like approach: `HMAC-SHA256(HMAC-SHA256(license_key, server_secret) + "\x01", server_secret)`
|
||||||
|
- Each license gets a unique 64-character hex secret
|
||||||
|
- Secrets are only shown when `WC_LICENSE_SERVER_SECRET` is configured
|
||||||
|
- Collapsible UI prevents accidental secret exposure
|
||||||
|
- If server secret is rotated, all customer secrets change automatically
|
||||||
|
|
||||||
|
**Security improvement:**
|
||||||
|
|
||||||
|
- Customers no longer need access to the master `WC_LICENSE_SERVER_SECRET`
|
||||||
|
- If one customer's secret is leaked, other customers are not affected
|
||||||
|
- Each license key derives its own unique verification secret
|
||||||
|
|
||||||
|
**Release v0.5.2:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.5.2.zip` (845 KB)
|
||||||
|
- SHA256: `2d61a78ac5ba0f1d115a6401e6dded5b872b18f5530027c371604cbd18e9e27c`
|
||||||
|
- Tagged as `v0.5.2` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-26 - Version 0.5.3 - Variable Licensed Products
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Major feature release adding support for WooCommerce variable products. Customers can now purchase licenses with different durations (monthly, yearly, lifetime) as product variations.
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
- `src/Product/LicensedVariableProduct.php` - Variable product class extending `WC_Product_Variable`
|
||||||
|
- `src/Product/LicensedProductVariation.php` - Variation class with license settings
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- New `licensed-variable` product type for selling licenses with different durations
|
||||||
|
- `LicensedVariableProduct` class extending WooCommerce variable products
|
||||||
|
- `LicensedProductVariation` class for individual variation license settings
|
||||||
|
- Variation-specific license duration fields in product edit page (days, max activations)
|
||||||
|
- Duration labels (Monthly, Quarterly, Yearly, Lifetime) displayed in checkout
|
||||||
|
- Variation ID tracking in order domain meta for proper license generation
|
||||||
|
- WooCommerce Blocks checkout updated to handle variations with duration labels
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Product/LicensedProductType.php` - Added licensed-variable type registration, variation hooks
|
||||||
|
- `src/License/LicenseManager.php` - Added `isLicensedProduct()` helper, variation support in `generateLicense()`
|
||||||
|
- `src/Plugin.php` - Updated license generation to handle variations
|
||||||
|
- `src/Checkout/CheckoutController.php` - Variation support in domain field rendering
|
||||||
|
- `src/Checkout/CheckoutBlocksIntegration.php` - Variation data in blocks checkout
|
||||||
|
- `src/Checkout/StoreApiExtension.php` - Variation ID in Store API schema
|
||||||
|
- `assets/js/checkout-blocks.js` - Variation handling in React components and DOM fallback
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Variable product type shows in WooCommerce product type selector as "Licensed Variable Product"
|
||||||
|
- Each variation can override parent's license duration and max activations
|
||||||
|
- Variations are always virtual (licensed products don't ship)
|
||||||
|
- `LicensedProductVariation::get_license_duration_label()` returns human-readable duration
|
||||||
|
- Order meta `_licensed_product_domains` now includes optional `variation_id` field
|
||||||
|
- License generation uses variation settings when `variation_id` is present in order item
|
||||||
|
- Backward compatible: existing simple licensed products continue to work unchanged
|
||||||
|
|||||||
@@ -863,3 +863,118 @@
|
|||||||
color: #2271b1;
|
color: #2271b1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Customer Secret Section */
|
||||||
|
.license-row-secret {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
padding-top: 0.75em;
|
||||||
|
border-top: 1px dashed #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35em;
|
||||||
|
padding: 0.4em 0.75em;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #ccc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle .toggle-arrow {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle[aria-expanded="true"] .toggle-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-content {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
padding: 1em;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-description {
|
||||||
|
margin: 0 0 0.75em 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-value-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-value {
|
||||||
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.75em;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn .dashicons {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.secret-value-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-value {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
* Multi-Domain Component
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +128,8 @@
|
|||||||
const [domains, setDomains] = useState(() => {
|
const [domains, setDomains] = useState(() => {
|
||||||
const init = {};
|
const init = {};
|
||||||
products.forEach(p => {
|
products.forEach(p => {
|
||||||
init[p.product_id] = Array(p.quantity).fill('');
|
const key = getProductKey(p);
|
||||||
|
init[key] = Array(p.quantity).fill('');
|
||||||
});
|
});
|
||||||
return init;
|
return init;
|
||||||
});
|
});
|
||||||
@@ -128,16 +139,16 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (productId, index, value) => {
|
const handleChange = (productKey, index, value) => {
|
||||||
const normalized = normalizeDomain(value);
|
const normalized = normalizeDomain(value);
|
||||||
const newDomains = { ...domains };
|
const newDomains = { ...domains };
|
||||||
if (!newDomains[productId]) newDomains[productId] = [];
|
if (!newDomains[productKey]) newDomains[productKey] = [];
|
||||||
newDomains[productId] = [...newDomains[productId]];
|
newDomains[productKey] = [...newDomains[productKey]];
|
||||||
newDomains[productId][index] = normalized;
|
newDomains[productKey][index] = normalized;
|
||||||
setDomains(newDomains);
|
setDomains(newDomains);
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
const key = `${productId}_${index}`;
|
const key = `${productKey}_${index}`;
|
||||||
const newErrors = { ...errors };
|
const newErrors = { ...errors };
|
||||||
if (normalized && !isValidDomain(normalized)) {
|
if (normalized && !isValidDomain(normalized)) {
|
||||||
newErrors[key] = settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product');
|
newErrors[key] = settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product');
|
||||||
@@ -145,14 +156,14 @@
|
|||||||
delete newErrors[key];
|
delete newErrors[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates within same product
|
// Check for duplicates within same product/variation
|
||||||
const productDomains = newDomains[productId].filter(d => d);
|
const productDomains = newDomains[productKey].filter(d => d);
|
||||||
const uniqueDomains = new Set(productDomains.map(d => normalizeDomain(d)));
|
const uniqueDomains = new Set(productDomains.map(d => normalizeDomain(d)));
|
||||||
if (productDomains.length !== uniqueDomains.size) {
|
if (productDomains.length !== uniqueDomains.size) {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
newDomains[productId].forEach((d, idx) => {
|
newDomains[productKey].forEach((d, idx) => {
|
||||||
const normalizedD = normalizeDomain(d);
|
const normalizedD = normalizeDomain(d);
|
||||||
const dupKey = `${productId}_${idx}`;
|
const dupKey = `${productKey}_${idx}`;
|
||||||
if (normalizedD && seen.has(normalizedD)) {
|
if (normalizedD && seen.has(normalizedD)) {
|
||||||
newErrors[dupKey] = settings.duplicateError || __('Each license requires a unique domain.', 'wc-licensed-product');
|
newErrors[dupKey] = settings.duplicateError || __('Each license requires a unique domain.', 'wc-licensed-product');
|
||||||
} else if (normalizedD) {
|
} else if (normalizedD) {
|
||||||
@@ -163,11 +174,19 @@
|
|||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
// Update hidden field
|
// Update hidden field with variation support
|
||||||
const data = Object.entries(newDomains).map(([pid, doms]) => ({
|
const data = products.map(p => {
|
||||||
product_id: parseInt(pid, 10),
|
const pKey = getProductKey(p);
|
||||||
|
const doms = newDomains[pKey] || [];
|
||||||
|
const entry = {
|
||||||
|
product_id: p.product_id,
|
||||||
domains: doms.filter(d => d),
|
domains: doms.filter(d => d),
|
||||||
})).filter(item => item.domains.length > 0);
|
};
|
||||||
|
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');
|
const hiddenInput = document.getElementById('wclp-domains-hidden');
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
@@ -192,10 +211,17 @@
|
|||||||
createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } },
|
createElement('p', { style: { marginBottom: '12px', color: '#666', fontSize: '0.9em' } },
|
||||||
settings.fieldDescription || __('Enter a unique domain for each license.', 'wc-licensed-product')
|
settings.fieldDescription || __('Enter a unique domain for each license.', 'wc-licensed-product')
|
||||||
),
|
),
|
||||||
products.map(product => createElement(
|
products.map(product => {
|
||||||
|
const productKey = getProductKey(product);
|
||||||
|
const durationLabel = product.duration_label || '';
|
||||||
|
const displayName = durationLabel
|
||||||
|
? `${product.name} (${durationLabel})`
|
||||||
|
: product.name;
|
||||||
|
|
||||||
|
return createElement(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
key: product.product_id,
|
key: productKey,
|
||||||
style: {
|
style: {
|
||||||
marginBottom: '16px',
|
marginBottom: '16px',
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
@@ -204,23 +230,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
createElement('strong', { style: { display: 'block', marginBottom: '8px' } },
|
createElement('strong', { style: { display: 'block', marginBottom: '8px' } },
|
||||||
product.name + (product.quantity > 1 ? ` (×${product.quantity})` : '')
|
displayName + (product.quantity > 1 ? ` ×${product.quantity}` : '')
|
||||||
),
|
),
|
||||||
Array.from({ length: product.quantity }, (_, i) => {
|
Array.from({ length: product.quantity }, (_, i) => {
|
||||||
const key = `${product.product_id}_${i}`;
|
const key = `${productKey}_${i}`;
|
||||||
return createElement(
|
return createElement(
|
||||||
'div',
|
'div',
|
||||||
{ key: i, style: { marginBottom: '8px' } },
|
{ key: i, style: { marginBottom: '8px' } },
|
||||||
createElement(TextControl, {
|
createElement(TextControl, {
|
||||||
label: (settings.licenseLabel || __('License %d:', 'wc-licensed-product')).replace('%d', i + 1),
|
label: (settings.licenseLabel || __('License %d:', 'wc-licensed-product')).replace('%d', i + 1),
|
||||||
value: domains[product.product_id]?.[i] || '',
|
value: domains[productKey]?.[i] || '',
|
||||||
onChange: (val) => handleChange(product.product_id, i, val),
|
onChange: (val) => handleChange(productKey, i, val),
|
||||||
placeholder: settings.fieldPlaceholder || 'example.com',
|
placeholder: settings.fieldPlaceholder || 'example.com',
|
||||||
help: errors[key] || '',
|
help: errors[key] || '',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)),
|
);
|
||||||
|
}),
|
||||||
createElement('input', {
|
createElement('input', {
|
||||||
type: 'hidden',
|
type: 'hidden',
|
||||||
id: 'wclp-domains-hidden',
|
id: 'wclp-domains-hidden',
|
||||||
@@ -291,10 +318,19 @@
|
|||||||
<p style="margin-bottom: 12px; color: #666; font-size: 0.9em;">
|
<p style="margin-bottom: 12px; color: #666; font-size: 0.9em;">
|
||||||
${settings.fieldDescription || 'Enter a unique domain for each license.'}
|
${settings.fieldDescription || 'Enter a unique domain for each license.'}
|
||||||
</p>
|
</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;">
|
<div style="margin-bottom: 16px; padding: 12px; background: #fff; border-radius: 4px;">
|
||||||
<strong style="display: block; margin-bottom: 8px;">
|
<strong style="display: block; margin-bottom: 8px;">
|
||||||
${product.name}${product.quantity > 1 ? ` (×${product.quantity})` : ''}
|
${displayName}${product.quantity > 1 ? ` ×${product.quantity}` : ''}
|
||||||
</strong>
|
</strong>
|
||||||
${Array.from({ length: product.quantity }, (_, i) => `
|
${Array.from({ length: product.quantity }, (_, i) => `
|
||||||
<div style="margin-bottom: 8px;">
|
<div style="margin-bottom: 8px;">
|
||||||
@@ -302,14 +338,20 @@
|
|||||||
${(settings.licenseLabel || 'License %d:').replace('%d', i + 1)}
|
${(settings.licenseLabel || 'License %d:').replace('%d', i + 1)}
|
||||||
</label>
|
</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="licensed_domains[${product.product_id}][${i}]"
|
name="licensed_domains[${productKey}][${i}]"
|
||||||
placeholder="${settings.fieldPlaceholder || 'example.com'}"
|
placeholder="${settings.fieldPlaceholder || 'example.com'}"
|
||||||
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
|
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>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
`).join('')}
|
`}).join('')}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
bindEvents: function() {
|
bindEvents: function() {
|
||||||
$(document).on('click', '.copy-license-btn', this.copyLicenseKey);
|
$(document).on('click', '.copy-license-btn', this.copyLicenseKey);
|
||||||
|
$(document).on('click', '.copy-secret-btn', this.copySecret);
|
||||||
|
|
||||||
// Transfer modal events
|
// Transfer modal events
|
||||||
$(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this));
|
$(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this));
|
||||||
@@ -28,6 +29,9 @@
|
|||||||
// Older versions toggle
|
// Older versions toggle
|
||||||
$(document).on('click', '.older-versions-toggle', this.toggleOlderVersions);
|
$(document).on('click', '.older-versions-toggle', this.toggleOlderVersions);
|
||||||
|
|
||||||
|
// Secret toggle
|
||||||
|
$(document).on('click', '.secret-toggle', this.toggleSecret);
|
||||||
|
|
||||||
// Close modal on escape key
|
// Close modal on escape key
|
||||||
$(document).on('keyup', function(e) {
|
$(document).on('keyup', function(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -50,6 +54,47 @@
|
|||||||
$list.slideToggle(200);
|
$list.slideToggle(200);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle secret visibility
|
||||||
|
*/
|
||||||
|
toggleSecret: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(this);
|
||||||
|
var $content = $btn.siblings('.secret-content');
|
||||||
|
var isExpanded = $btn.attr('aria-expanded') === 'true';
|
||||||
|
|
||||||
|
$btn.attr('aria-expanded', !isExpanded);
|
||||||
|
$content.slideToggle(200);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy secret to clipboard
|
||||||
|
*/
|
||||||
|
copySecret: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(this);
|
||||||
|
var secret = $btn.data('secret');
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use modern clipboard API if available
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(secret)
|
||||||
|
.then(function() {
|
||||||
|
WCLicensedProductFrontend.showCopyFeedback($btn, true);
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
WCLicensedProductFrontend.fallbackCopy(secret, $btn);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
WCLicensedProductFrontend.fallbackCopy(secret, $btn);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy license key to clipboard
|
* Copy license key to clipboard
|
||||||
*/
|
*/
|
||||||
|
|||||||
12
composer.lock
generated
12
composer.lock
generated
@@ -380,16 +380,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-client",
|
"name": "symfony/http-client",
|
||||||
"version": "v7.4.3",
|
"version": "v7.4.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/http-client.git",
|
"url": "https://github.com/symfony/http-client.git",
|
||||||
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616"
|
"reference": "d63c23357d74715a589454c141c843f0172bec6c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616",
|
"url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c",
|
||||||
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616",
|
"reference": "d63c23357d74715a589454c141c843f0172bec6c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
"http"
|
"http"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/http-client/tree/v7.4.3"
|
"source": "https://github.com/symfony/http-client/tree/v7.4.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -477,7 +477,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-23T14:50:43+00:00"
|
"time": "2026-01-23T16:34:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-client-contracts",
|
"name": "symfony/http-client-contracts",
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ The security model works as follows:
|
|||||||
|
|
||||||
1. Server generates a unique signature for each response using HMAC-SHA256
|
1. Server generates a unique signature for each response using HMAC-SHA256
|
||||||
2. Signature includes a timestamp to prevent replay attacks
|
2. Signature includes a timestamp to prevent replay attacks
|
||||||
3. Client verifies the signature using a shared secret
|
3. Each license key has a unique derived secret (not the master secret)
|
||||||
4. Invalid signatures cause the client to reject the response
|
4. Client verifies the signature using their per-license secret
|
||||||
|
5. Invalid signatures cause the client to reject the response
|
||||||
|
|
||||||
This prevents attackers from:
|
This prevents attackers from:
|
||||||
|
|
||||||
- Faking valid license responses
|
- Faking valid license responses
|
||||||
- Replaying old responses
|
- Replaying old responses
|
||||||
- Tampering with response data
|
- Tampering with response data
|
||||||
|
- Using one customer's secret to verify another customer's responses
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -323,13 +325,49 @@ Adjust if needed:
|
|||||||
$signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes
|
$signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Per-License Secrets
|
||||||
|
|
||||||
|
Each customer receives a unique secret derived from their license key. This means:
|
||||||
|
|
||||||
|
- Customers only know their own secret, not the master server secret
|
||||||
|
- If one customer's secret is leaked, other customers are not affected
|
||||||
|
- The server uses HKDF-like derivation to create unique secrets
|
||||||
|
|
||||||
|
#### How Customers Get Their Secret
|
||||||
|
|
||||||
|
Customers can find their per-license verification secret in their account:
|
||||||
|
|
||||||
|
1. Log in to the store
|
||||||
|
2. Go to My Account > Licenses
|
||||||
|
3. Click "API Verification Secret" under any license
|
||||||
|
4. Copy the 64-character hex string
|
||||||
|
|
||||||
|
This secret is automatically derived from the customer's license key and the server's master secret.
|
||||||
|
|
||||||
|
#### Using the Customer Secret
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
// Customer uses their per-license secret (from account page)
|
||||||
|
$client = new SecureLicenseClient(
|
||||||
|
httpClient: HttpClient::create(),
|
||||||
|
baseUrl: 'https://shop.example.com',
|
||||||
|
serverSecret: 'customer-secret-from-account-page', // 64 hex chars
|
||||||
|
);
|
||||||
|
|
||||||
|
$info = $client->validate('XXXX-XXXX-XXXX-XXXX', 'example.com');
|
||||||
|
```
|
||||||
|
|
||||||
### Secret Key Rotation
|
### Secret Key Rotation
|
||||||
|
|
||||||
To rotate the server secret:
|
To rotate the server secret:
|
||||||
|
|
||||||
1. Deploy new secret to server
|
1. Deploy new secret to server
|
||||||
2. Update client configurations
|
2. All per-license secrets change automatically (they're derived)
|
||||||
3. Old signatures become invalid immediately
|
3. Customers must copy their new secret from their account page
|
||||||
|
4. Old signatures become invalid immediately
|
||||||
|
|
||||||
For zero-downtime rotation, implement versioned secrets:
|
For zero-downtime rotation, implement versioned secrets:
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -4,8 +4,8 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WC Licensed Product 0.5.0\n"
|
"Project-Id-Version: WC Licensed Product 0.5.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
||||||
"POT-Creation-Date: 2026-01-25 18:32+0100\n"
|
"POT-Creation-Date: 2026-01-26 16:08+0100\n"
|
||||||
"PO-Revision-Date: 2026-01-25T18:30:00+00:00\n"
|
"PO-Revision-Date: 2026-01-25T18:30:00+00:00\n"
|
||||||
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
|
"Last-Translator: Marco Graetsch <magdev3.0@gmail.com>\n"
|
||||||
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
|
"Language-Team: German (Switzerland) <de_CH@li.org>\n"
|
||||||
@@ -248,7 +248,7 @@ msgstr "Version erfolgreich aktualisiert."
|
|||||||
#: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200
|
#: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200
|
||||||
#: src/Admin/OrderLicenseController.php:149
|
#: src/Admin/OrderLicenseController.php:149
|
||||||
#: src/Admin/OrderLicenseController.php:281
|
#: src/Admin/OrderLicenseController.php:281
|
||||||
#: src/Frontend/AccountController.php:90
|
#: src/Frontend/AccountController.php:91
|
||||||
msgid "Licenses"
|
msgid "Licenses"
|
||||||
msgstr "Lizenzen"
|
msgstr "Lizenzen"
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ msgstr "Bearbeiten"
|
|||||||
#: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341
|
#: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341
|
||||||
#: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382
|
#: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382
|
||||||
#: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:244
|
#: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:244
|
||||||
#: src/Frontend/AccountController.php:384
|
#: src/Frontend/AccountController.php:387
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Abbrechen"
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
@@ -310,17 +310,19 @@ msgstr "Speichern"
|
|||||||
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
|
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
|
||||||
#: src/Admin/DashboardWidgetController.php:136
|
#: src/Admin/DashboardWidgetController.php:136
|
||||||
#: src/Admin/OrderLicenseController.php:260
|
#: src/Admin/OrderLicenseController.php:260
|
||||||
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:110
|
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:136
|
||||||
#: src/Product/LicensedProductType.php:158
|
#: src/Product/LicensedProductType.php:184
|
||||||
#: src/Frontend/AccountController.php:283
|
#: src/Product/LicensedProductType.php:379
|
||||||
|
#: src/Product/LicensedProductVariation.php:139
|
||||||
|
#: src/Frontend/AccountController.php:286
|
||||||
msgid "Lifetime"
|
msgid "Lifetime"
|
||||||
msgstr "Lebenslang"
|
msgstr "Lebenslang"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:422
|
#: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:425
|
||||||
msgid "Copied!"
|
msgid "Copied!"
|
||||||
msgstr "Kopiert!"
|
msgstr "Kopiert!"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:423
|
#: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:426
|
||||||
msgid "Copy failed"
|
msgid "Copy failed"
|
||||||
msgstr "Kopieren fehlgeschlagen"
|
msgstr "Kopieren fehlgeschlagen"
|
||||||
|
|
||||||
@@ -408,7 +410,7 @@ msgstr "Lizenzschlüssel und Domain sind erforderlich."
|
|||||||
#: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549
|
#: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549
|
||||||
#: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621
|
#: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621
|
||||||
#: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:469
|
#: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:469
|
||||||
#: src/Frontend/AccountController.php:439
|
#: src/Frontend/AccountController.php:442
|
||||||
msgid "Security check failed."
|
msgid "Security check failed."
|
||||||
msgstr "Sicherheitsüberprüfung fehlgeschlagen."
|
msgstr "Sicherheitsüberprüfung fehlgeschlagen."
|
||||||
|
|
||||||
@@ -677,8 +679,8 @@ msgstr "Kunde"
|
|||||||
|
|
||||||
#: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445
|
#: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445
|
||||||
#: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:205
|
#: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:205
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
#: src/Checkout/CheckoutBlocksIntegration.php:130
|
||||||
#: src/Checkout/CheckoutController.php:122
|
#: src/Checkout/CheckoutController.php:161
|
||||||
#: src/Email/LicenseEmailController.php:288
|
#: src/Email/LicenseEmailController.php:288
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "Domain"
|
msgstr "Domain"
|
||||||
@@ -698,7 +700,7 @@ msgstr "Läuft ab"
|
|||||||
msgid "No licenses found."
|
msgid "No licenses found."
|
||||||
msgstr "Keine Lizenzen gefunden."
|
msgstr "Keine Lizenzen gefunden."
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:260
|
#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:263
|
||||||
msgid "Copy to clipboard"
|
msgid "Copy to clipboard"
|
||||||
msgstr "In Zwischenablage kopieren"
|
msgstr "In Zwischenablage kopieren"
|
||||||
|
|
||||||
@@ -718,7 +720,7 @@ msgstr "Lizenz gegen API testen"
|
|||||||
msgid "Test"
|
msgid "Test"
|
||||||
msgstr "Testen"
|
msgstr "Testen"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:267
|
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:270
|
||||||
msgid "Transfer to new domain"
|
msgid "Transfer to new domain"
|
||||||
msgstr "Auf neue Domain übertragen"
|
msgstr "Auf neue Domain übertragen"
|
||||||
|
|
||||||
@@ -746,27 +748,27 @@ msgstr "Lizenzvalidierungstest"
|
|||||||
msgid "Testing license..."
|
msgid "Testing license..."
|
||||||
msgstr "Lizenz wird geprüft..."
|
msgstr "Lizenz wird geprüft..."
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:362
|
#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:365
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Schliessen"
|
msgstr "Schliessen"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:363
|
#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:366
|
||||||
msgid "Transfer License to New Domain"
|
msgid "Transfer License to New Domain"
|
||||||
msgstr "Lizenz auf neue Domain übertragen"
|
msgstr "Lizenz auf neue Domain übertragen"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:368
|
#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:371
|
||||||
msgid "Current Domain"
|
msgid "Current Domain"
|
||||||
msgstr "Aktuelle Domain"
|
msgstr "Aktuelle Domain"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:373
|
#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:376
|
||||||
msgid "New Domain"
|
msgid "New Domain"
|
||||||
msgstr "Neue Domain"
|
msgstr "Neue Domain"
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:377
|
#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:380
|
||||||
msgid "Enter the new domain without http:// or www."
|
msgid "Enter the new domain without http:// or www."
|
||||||
msgstr "Geben Sie die neue Domain ohne http:// oder www ein."
|
msgstr "Geben Sie die neue Domain ohne http:// oder www ein."
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:382
|
#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:385
|
||||||
msgid "Transfer License"
|
msgid "Transfer License"
|
||||||
msgstr "Lizenz übertragen"
|
msgstr "Lizenz übertragen"
|
||||||
|
|
||||||
@@ -954,11 +956,11 @@ msgid "Domains specified during checkout (multi-domain order)."
|
|||||||
msgstr "Bei der Bestellung angegebene Domains (Multi-Domain-Bestellung)."
|
msgstr "Bei der Bestellung angegebene Domains (Multi-Domain-Bestellung)."
|
||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:119
|
#: src/Admin/OrderLicenseController.php:119
|
||||||
#: src/Checkout/CheckoutController.php:436
|
#: src/Checkout/CheckoutController.php:530
|
||||||
#: src/Checkout/CheckoutController.php:486
|
#: src/Checkout/CheckoutController.php:591
|
||||||
#: src/Checkout/CheckoutController.php:496 src/License/LicenseManager.php:806
|
#: src/Checkout/CheckoutController.php:613 src/License/LicenseManager.php:878
|
||||||
#: src/Product/VersionManager.php:349 src/Product/VersionManager.php:361
|
#: src/Product/VersionManager.php:349 src/Product/VersionManager.php:361
|
||||||
#: src/Frontend/AccountController.php:146
|
#: src/Frontend/AccountController.php:148
|
||||||
#: src/Email/LicenseExpirationEmail.php:107
|
#: src/Email/LicenseExpirationEmail.php:107
|
||||||
#: src/Email/LicenseExpiredEmail.php:99
|
#: src/Email/LicenseExpiredEmail.php:99
|
||||||
msgid "Unknown Product"
|
msgid "Unknown Product"
|
||||||
@@ -973,10 +975,10 @@ msgstr ""
|
|||||||
"automatisch bestehende Lizenz-Domains."
|
"automatisch bestehende Lizenz-Domains."
|
||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:137
|
#: src/Admin/OrderLicenseController.php:137
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:83
|
#: src/Checkout/CheckoutBlocksIntegration.php:84
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:119
|
#: src/Checkout/CheckoutBlocksIntegration.php:120
|
||||||
#: src/Checkout/CheckoutController.php:130
|
#: src/Checkout/CheckoutController.php:169
|
||||||
#: src/Checkout/CheckoutController.php:186
|
#: src/Checkout/CheckoutController.php:235
|
||||||
msgid "example.com"
|
msgid "example.com"
|
||||||
msgstr "beispiel.ch"
|
msgstr "beispiel.ch"
|
||||||
|
|
||||||
@@ -1044,9 +1046,9 @@ msgid "Error. Please try again."
|
|||||||
msgstr "Fehler. Bitte versuchen Sie es erneut."
|
msgstr "Fehler. Bitte versuchen Sie es erneut."
|
||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:373
|
#: src/Admin/OrderLicenseController.php:373
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:126
|
#: src/Checkout/CheckoutBlocksIntegration.php:127
|
||||||
#: src/Frontend/AccountController.php:427
|
#: src/Frontend/AccountController.php:430
|
||||||
#: src/Frontend/AccountController.php:459
|
#: src/Frontend/AccountController.php:462
|
||||||
msgid "Please enter a valid domain."
|
msgid "Please enter a valid domain."
|
||||||
msgstr "Bitte geben Sie eine gültige Domain ein."
|
msgstr "Bitte geben Sie eine gültige Domain ein."
|
||||||
|
|
||||||
@@ -1070,7 +1072,7 @@ msgstr "Bestellungs-Domain aktualisiert."
|
|||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:449
|
#: src/Admin/OrderLicenseController.php:449
|
||||||
#: src/Frontend/DownloadController.php:117
|
#: src/Frontend/DownloadController.php:117
|
||||||
#: src/Frontend/AccountController.php:465
|
#: src/Frontend/AccountController.php:468
|
||||||
msgid "License not found."
|
msgid "License not found."
|
||||||
msgstr "Lizenz nicht gefunden."
|
msgstr "Lizenz nicht gefunden."
|
||||||
|
|
||||||
@@ -1285,7 +1287,7 @@ msgid "Too many requests. Please try again later."
|
|||||||
msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
|
msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut."
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
|
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
|
||||||
#: src/License/LicenseManager.php:403
|
#: src/License/LicenseManager.php:475
|
||||||
msgid "License key not found."
|
msgid "License key not found."
|
||||||
msgstr "Lizenzschlüssel nicht gefunden."
|
msgstr "Lizenzschlüssel nicht gefunden."
|
||||||
|
|
||||||
@@ -1309,69 +1311,69 @@ msgstr "Lizenz konnte nicht aktiviert werden."
|
|||||||
msgid "License activated successfully."
|
msgid "License activated successfully."
|
||||||
msgstr "Lizenz erfolgreich aktiviert."
|
msgstr "Lizenz erfolgreich aktiviert."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:78
|
#: src/Checkout/CheckoutBlocksIntegration.php:79
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:125
|
#: src/Checkout/CheckoutBlocksIntegration.php:126
|
||||||
#: src/Checkout/CheckoutController.php:119
|
#: src/Checkout/CheckoutController.php:158
|
||||||
msgid "License Domain"
|
msgid "License Domain"
|
||||||
msgstr "Lizenz-Domain"
|
msgstr "Lizenz-Domain"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:85
|
#: src/Checkout/CheckoutBlocksIntegration.php:86
|
||||||
msgid "Enter a valid domain (without http:// or www)"
|
msgid "Enter a valid domain (without http:// or www)"
|
||||||
msgstr "Geben Sie eine gültige Domain ein (ohne http:// oder www)"
|
msgstr "Geben Sie eine gültige Domain ein (ohne http:// oder www)"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:121
|
#: src/Checkout/CheckoutBlocksIntegration.php:122
|
||||||
#: src/Checkout/CheckoutController.php:150
|
#: src/Checkout/CheckoutController.php:189
|
||||||
msgid "Enter a unique domain for each license (without http:// or www)."
|
msgid "Enter a unique domain for each license (without http:// or www)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Geben Sie für jede Lizenz eine eindeutige Domain ein (ohne http:// oder www)."
|
"Geben Sie für jede Lizenz eine eindeutige Domain ein (ohne http:// oder www)."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:122
|
#: src/Checkout/CheckoutBlocksIntegration.php:123
|
||||||
#: src/Checkout/CheckoutController.php:134
|
#: src/Checkout/CheckoutController.php:173
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter the domain where you will use the license (without http:// or www)."
|
"Enter the domain where you will use the license (without http:// or www)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Geben Sie die Domain ein, auf der Sie die Lizenz verwenden möchten (ohne "
|
"Geben Sie die Domain ein, auf der Sie die Lizenz verwenden möchten (ohne "
|
||||||
"http:// oder www)."
|
"http:// oder www)."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:124
|
#: src/Checkout/CheckoutBlocksIntegration.php:125
|
||||||
#: src/Checkout/CheckoutController.php:148
|
#: src/Checkout/CheckoutController.php:187
|
||||||
msgid "License Domains"
|
msgid "License Domains"
|
||||||
msgstr "Lizenz-Domains"
|
msgstr "Lizenz-Domains"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:127
|
#: src/Checkout/CheckoutBlocksIntegration.php:128
|
||||||
msgid "Each license requires a unique domain."
|
msgid "Each license requires a unique domain."
|
||||||
msgstr "Jede Lizenz erfordert eine eindeutige Domain."
|
msgstr "Jede Lizenz erfordert eine eindeutige Domain."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:128
|
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
||||||
#: src/Checkout/CheckoutController.php:175
|
#: src/Checkout/CheckoutController.php:224
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "License %d:"
|
msgid "License %d:"
|
||||||
msgstr "Lizenz %d:"
|
msgstr "Lizenz %d:"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:123
|
#: src/Checkout/CheckoutController.php:162
|
||||||
#: src/Checkout/CheckoutController.php:179
|
#: src/Checkout/CheckoutController.php:228
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "erforderlich"
|
msgstr "erforderlich"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:258
|
#: src/Checkout/CheckoutController.php:323
|
||||||
msgid "Please enter a domain for your license."
|
msgid "Please enter a domain for your license."
|
||||||
msgstr "Bitte geben Sie eine Domain für Ihre Lizenz ein."
|
msgstr "Bitte geben Sie eine Domain für Ihre Lizenz ein."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:264
|
#: src/Checkout/CheckoutController.php:329
|
||||||
msgid "Please enter a valid domain for your license."
|
msgid "Please enter a valid domain for your license."
|
||||||
msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz ein."
|
msgstr "Bitte geben Sie eine gültige Domain für Ihre Lizenz ein."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:287
|
#: src/Checkout/CheckoutController.php:356
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Please enter a domain for %1$s (License %2$d)."
|
msgid "Please enter a domain for %1$s (License %2$d)."
|
||||||
msgstr "Bitte geben Sie eine Domain für %1$s (Lizenz %2$d) ein."
|
msgstr "Bitte geben Sie eine Domain für %1$s (Lizenz %2$d) ein."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:302
|
#: src/Checkout/CheckoutController.php:371
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Please enter a valid domain for %1$s (License %2$d)."
|
msgid "Please enter a valid domain for %1$s (License %2$d)."
|
||||||
msgstr "Bitte geben Sie eine gültige Domain für %1$s (Lizenz %2$d) ein."
|
msgstr "Bitte geben Sie eine gültige Domain für %1$s (Lizenz %2$d) ein."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:316
|
#: src/Checkout/CheckoutController.php:385
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The domain \"%1$s\" is used multiple times for %2$s. Each license requires a "
|
"The domain \"%1$s\" is used multiple times for %2$s. Each license requires a "
|
||||||
@@ -1380,23 +1382,29 @@ msgstr ""
|
|||||||
"Die Domain \"%1$s\" wird mehrfach für %2$s verwendet. Jede Lizenz erfordert "
|
"Die Domain \"%1$s\" wird mehrfach für %2$s verwendet. Jede Lizenz erfordert "
|
||||||
"eine eindeutige Domain."
|
"eine eindeutige Domain."
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:419
|
#: src/Checkout/CheckoutController.php:500
|
||||||
#: src/Checkout/CheckoutController.php:466
|
#: src/Checkout/CheckoutController.php:561
|
||||||
#: src/Checkout/CheckoutController.php:470
|
#: src/Checkout/CheckoutController.php:565
|
||||||
msgid "License Domain:"
|
msgid "License Domain:"
|
||||||
msgstr "Lizenz-Domain:"
|
msgstr "Lizenz-Domain:"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:432
|
#: src/Checkout/CheckoutController.php:513
|
||||||
#: src/Checkout/CheckoutController.php:483
|
#: src/Checkout/CheckoutController.php:578
|
||||||
#: src/Checkout/CheckoutController.php:492
|
#: src/Checkout/CheckoutController.php:599
|
||||||
msgid "License Domains:"
|
msgid "License Domains:"
|
||||||
msgstr "Lizenz-Domains:"
|
msgstr "Lizenz-Domains:"
|
||||||
|
|
||||||
|
#: src/Checkout/CheckoutController.php:522
|
||||||
|
#: src/Checkout/CheckoutController.php:585
|
||||||
|
#: src/Checkout/CheckoutController.php:607
|
||||||
|
msgid "Unknown Variation"
|
||||||
|
msgstr "Unbekannte Variante"
|
||||||
|
|
||||||
#: src/Checkout/StoreApiExtension.php:93
|
#: src/Checkout/StoreApiExtension.php:93
|
||||||
msgid "Domains for license activation by product"
|
msgid "Domains for license activation by product"
|
||||||
msgstr "Domains für Lizenz-Aktivierung nach Produkt"
|
msgstr "Domains für Lizenz-Aktivierung nach Produkt"
|
||||||
|
|
||||||
#: src/Checkout/StoreApiExtension.php:117
|
#: src/Checkout/StoreApiExtension.php:120
|
||||||
msgid "Domain for license activation"
|
msgid "Domain for license activation"
|
||||||
msgstr "Domain für Lizenz-Aktivierung"
|
msgstr "Domain für Lizenz-Aktivierung"
|
||||||
|
|
||||||
@@ -1408,67 +1416,73 @@ msgstr "Lizenzeinstellungen nicht konfiguriert."
|
|||||||
msgid "Could not connect to license server."
|
msgid "Could not connect to license server."
|
||||||
msgstr "Verbindung zum Lizenzserver konnte nicht hergestellt werden."
|
msgstr "Verbindung zum Lizenzserver konnte nicht hergestellt werden."
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:412
|
#: src/License/LicenseManager.php:484
|
||||||
msgid "This license has been revoked."
|
msgid "This license has been revoked."
|
||||||
msgstr "Diese Lizenz wurde widerrufen."
|
msgstr "Diese Lizenz wurde widerrufen."
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:422
|
#: src/License/LicenseManager.php:494
|
||||||
msgid "This license has expired."
|
msgid "This license has expired."
|
||||||
msgstr "Diese Lizenz ist abgelaufen."
|
msgstr "Diese Lizenz ist abgelaufen."
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:430
|
#: src/License/LicenseManager.php:502
|
||||||
msgid "This license is inactive."
|
msgid "This license is inactive."
|
||||||
msgstr "Diese Lizenz ist inaktiv."
|
msgstr "Diese Lizenz ist inaktiv."
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:440
|
#: src/License/LicenseManager.php:512
|
||||||
msgid "This license is not valid for this domain."
|
msgid "This license is not valid for this domain."
|
||||||
msgstr "Diese Lizenz ist für diese Domain nicht gültig."
|
msgstr "Diese Lizenz ist für diese Domain nicht gültig."
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:61
|
#: src/Product/LicensedProductType.php:72
|
||||||
msgid "Licensed Product"
|
msgid "Licensed Product"
|
||||||
msgstr "Lizensiertes Produkt"
|
msgstr "Lizensiertes Produkt"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:82
|
#: src/Product/LicensedProductType.php:73
|
||||||
|
msgid "Licensed Variable Product"
|
||||||
|
msgstr "Lizensiertes variables Produkt"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:108
|
||||||
msgid "License Settings"
|
msgid "License Settings"
|
||||||
msgstr "Lizenz-Einstellungen"
|
msgstr "Lizenz-Einstellungen"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:109
|
#: src/Product/LicensedProductType.php:135
|
||||||
|
#: src/Product/LicensedProductType.php:378
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d days"
|
msgid "%d days"
|
||||||
msgstr "%d Tage"
|
msgstr "%d Tage"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:119
|
#: src/Product/LicensedProductType.php:145
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Leave fields empty to use default settings from %s."
|
msgid "Leave fields empty to use default settings from %s."
|
||||||
msgstr "Felder leer lassen, um Standardeinstellungen von %s zu verwenden."
|
msgstr "Felder leer lassen, um Standardeinstellungen von %s zu verwenden."
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:121
|
#: src/Product/LicensedProductType.php:147
|
||||||
msgid "WooCommerce > Settings > Licensed Products"
|
msgid "WooCommerce > Settings > Licensed Products"
|
||||||
msgstr "WooCommerce > Einstellungen > Lizensierte Produkte"
|
msgstr "WooCommerce > Einstellungen > Lizensierte Produkte"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:128
|
#: src/Product/LicensedProductType.php:154
|
||||||
|
#: src/Product/LicensedProductType.php:396
|
||||||
msgid "Max Activations"
|
msgid "Max Activations"
|
||||||
msgstr "Max. Aktivierungen"
|
msgstr "Max. Aktivierungen"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:131
|
#: src/Product/LicensedProductType.php:157
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Maximum number of domain activations per license. Default: %d"
|
msgid "Maximum number of domain activations per license. Default: %d"
|
||||||
msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d"
|
msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:146
|
#: src/Product/LicensedProductType.php:172
|
||||||
msgid "License Validity (Days)"
|
msgid "License Validity (Days)"
|
||||||
msgstr "Lizenz-Gültigkeit (Tage)"
|
msgstr "Lizenz-Gültigkeit (Tage)"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:149
|
#: src/Product/LicensedProductType.php:175
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Number of days the license is valid. Leave empty for default (%s)."
|
msgid "Number of days the license is valid. Leave empty for default (%s)."
|
||||||
msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s)."
|
msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s)."
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:164
|
#: src/Product/LicensedProductType.php:190
|
||||||
msgid "Bind to Major Version"
|
msgid "Bind to Major Version"
|
||||||
msgstr "An Hauptversion binden"
|
msgstr "An Hauptversion binden"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:167
|
#: src/Product/LicensedProductType.php:193
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"If enabled, licenses are bound to the major version at purchase time. "
|
"If enabled, licenses are bound to the major version at purchase time. "
|
||||||
@@ -1477,18 +1491,38 @@ msgstr ""
|
|||||||
"Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt "
|
"Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt "
|
||||||
"gebunden. Standard: %s"
|
"gebunden. Standard: %s"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:168
|
#: src/Product/LicensedProductType.php:194
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:168
|
#: src/Product/LicensedProductType.php:194
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr "Nein"
|
msgstr "Nein"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:288
|
#: src/Product/LicensedProductType.php:321
|
||||||
msgid "Version:"
|
msgid "Version:"
|
||||||
msgstr "Version:"
|
msgstr "Version:"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:349
|
||||||
|
msgid "Licensed products are always virtual"
|
||||||
|
msgstr "Lizenzierte Produkte sind immer virtuell"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:351
|
||||||
|
msgid "Virtual"
|
||||||
|
msgstr "Virtuell"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:384
|
||||||
|
msgid "License Duration (Days)"
|
||||||
|
msgstr "Lizenz-Gültigkeit (Tage)"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:393
|
||||||
|
msgid "Leave empty for parent default. 0 = Lifetime."
|
||||||
|
msgstr "Leer lassen für übergeordneten Standard. 0 = Lebenslang."
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:405
|
||||||
|
msgid "Leave empty for parent default."
|
||||||
|
msgstr "Leer lassen für übergeordneten Standard."
|
||||||
|
|
||||||
#: src/Product/VersionManager.php:166
|
#: src/Product/VersionManager.php:166
|
||||||
msgid "Attachment file not found."
|
msgid "Attachment file not found."
|
||||||
msgstr "Anhangs-Datei nicht gefunden."
|
msgstr "Anhangs-Datei nicht gefunden."
|
||||||
@@ -1498,6 +1532,25 @@ msgstr "Anhangs-Datei nicht gefunden."
|
|||||||
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
||||||
msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s"
|
msgstr "Datei-Prüfsumme stimmt nicht überein. Erwartet: %1$s, Erhalten: %2$s"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:143
|
||||||
|
msgid "Monthly"
|
||||||
|
msgstr "Monatlich"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:147
|
||||||
|
msgid "Quarterly"
|
||||||
|
msgstr "Vierteljährlich"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:151
|
||||||
|
msgid "Yearly"
|
||||||
|
msgstr "Jährlich"
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:156
|
||||||
|
#, php-format
|
||||||
|
msgid "%d day"
|
||||||
|
msgid_plural "%d days"
|
||||||
|
msgstr[0] "%d Tag"
|
||||||
|
msgstr[1] "%d Tage"
|
||||||
|
|
||||||
#: src/Frontend/DownloadController.php:77
|
#: src/Frontend/DownloadController.php:77
|
||||||
#: src/Frontend/DownloadController.php:101
|
#: src/Frontend/DownloadController.php:101
|
||||||
msgid "Invalid download link."
|
msgid "Invalid download link."
|
||||||
@@ -1549,48 +1602,48 @@ msgstr "Keine Download-Datei für diese Version verfügbar."
|
|||||||
msgid "Download file not found."
|
msgid "Download file not found."
|
||||||
msgstr "Download-Datei nicht gefunden."
|
msgstr "Download-Datei nicht gefunden."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:104
|
#: src/Frontend/AccountController.php:105
|
||||||
msgid "Please log in to view your licenses."
|
msgid "Please log in to view your licenses."
|
||||||
msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen."
|
msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:220
|
#: src/Frontend/AccountController.php:223
|
||||||
msgid "You have no licenses yet."
|
msgid "You have no licenses yet."
|
||||||
msgstr "Sie haben noch keine Lizenzen."
|
msgstr "Sie haben noch keine Lizenzen."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:242
|
#: src/Frontend/AccountController.php:245
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Order #%s"
|
msgid "Order #%s"
|
||||||
msgstr "Bestellung #%s"
|
msgstr "Bestellung #%s"
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:293
|
#: src/Frontend/AccountController.php:296
|
||||||
msgid "Available Downloads"
|
msgid "Available Downloads"
|
||||||
msgstr "Verfügbare Downloads"
|
msgstr "Verfügbare Downloads"
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:302
|
#: src/Frontend/AccountController.php:305
|
||||||
#: src/Frontend/AccountController.php:335
|
#: src/Frontend/AccountController.php:338
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Version %s"
|
msgid "Version %s"
|
||||||
msgstr "Version %s"
|
msgstr "Version %s"
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:304
|
#: src/Frontend/AccountController.php:307
|
||||||
msgid "Latest"
|
msgid "Latest"
|
||||||
msgstr "Neueste"
|
msgstr "Neueste"
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:324
|
#: src/Frontend/AccountController.php:327
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Older versions (%d)"
|
msgid "Older versions (%d)"
|
||||||
msgstr "Ältere Versionen (%d)"
|
msgstr "Ältere Versionen (%d)"
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:424
|
#: src/Frontend/AccountController.php:427
|
||||||
#: src/Frontend/AccountController.php:491
|
#: src/Frontend/AccountController.php:494
|
||||||
msgid "License transferred successfully!"
|
msgid "License transferred successfully!"
|
||||||
msgstr "Lizenz erfolgreich übertragen!"
|
msgstr "Lizenz erfolgreich übertragen!"
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:425
|
#: src/Frontend/AccountController.php:428
|
||||||
msgid "Transfer failed. Please try again."
|
msgid "Transfer failed. Please try again."
|
||||||
msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
msgstr "Übertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:426
|
#: src/Frontend/AccountController.php:429
|
||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure you want to transfer this license to a new domain? This action "
|
"Are you sure you want to transfer this license to a new domain? This action "
|
||||||
"cannot be undone."
|
"cannot be undone."
|
||||||
@@ -1598,31 +1651,31 @@ msgstr ""
|
|||||||
"Sind Sie sicher, dass Sie diese Lizenz auf eine neue Domain übertragen "
|
"Sind Sie sicher, dass Sie diese Lizenz auf eine neue Domain übertragen "
|
||||||
"möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
"möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:445
|
#: src/Frontend/AccountController.php:448
|
||||||
msgid "Please log in to transfer a license."
|
msgid "Please log in to transfer a license."
|
||||||
msgstr "Bitte melden Sie sich an, um eine Lizenz zu übertragen."
|
msgstr "Bitte melden Sie sich an, um eine Lizenz zu übertragen."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:451
|
#: src/Frontend/AccountController.php:454
|
||||||
msgid "Invalid license."
|
msgid "Invalid license."
|
||||||
msgstr "Ungültige Lizenz."
|
msgstr "Ungültige Lizenz."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:469
|
#: src/Frontend/AccountController.php:472
|
||||||
msgid "You do not have permission to transfer this license."
|
msgid "You do not have permission to transfer this license."
|
||||||
msgstr "Sie haben keine Berechtigung, diese Lizenz zu übertragen."
|
msgstr "Sie haben keine Berechtigung, diese Lizenz zu übertragen."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:474
|
#: src/Frontend/AccountController.php:477
|
||||||
msgid "Revoked licenses cannot be transferred."
|
msgid "Revoked licenses cannot be transferred."
|
||||||
msgstr "Widerrufene Lizenzen können nicht übertragen werden."
|
msgstr "Widerrufene Lizenzen können nicht übertragen werden."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:478
|
#: src/Frontend/AccountController.php:481
|
||||||
msgid "Expired licenses cannot be transferred."
|
msgid "Expired licenses cannot be transferred."
|
||||||
msgstr "Abgelaufene Lizenzen können nicht übertragen werden."
|
msgstr "Abgelaufene Lizenzen können nicht übertragen werden."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:483
|
#: src/Frontend/AccountController.php:486
|
||||||
msgid "The new domain is the same as the current domain."
|
msgid "The new domain is the same as the current domain."
|
||||||
msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain."
|
msgstr "Die neue Domain ist dieselbe wie die aktuelle Domain."
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:495
|
#: src/Frontend/AccountController.php:498
|
||||||
msgid "Failed to transfer license. Please try again."
|
msgid "Failed to transfer license. Please try again."
|
||||||
msgstr "Lizenzübertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
msgstr "Lizenzübertragung fehlgeschlagen. Bitte versuchen Sie es erneut."
|
||||||
|
|
||||||
@@ -1837,18 +1890,18 @@ msgstr ""
|
|||||||
msgid "YOUR LICENSE KEYS"
|
msgid "YOUR LICENSE KEYS"
|
||||||
msgstr "IHRE LIZENZSCHLÜSSEL"
|
msgstr "IHRE LIZENZSCHLÜSSEL"
|
||||||
|
|
||||||
#: src/Plugin.php:318
|
#: src/Plugin.php:336
|
||||||
msgid "WC Licensed Product"
|
msgid "WC Licensed Product"
|
||||||
msgstr "WC Licensed Product"
|
msgstr "WC Licensed Product"
|
||||||
|
|
||||||
#: src/Plugin.php:319
|
#: src/Plugin.php:337
|
||||||
msgid ""
|
msgid ""
|
||||||
"Plugin license is not configured or invalid. Frontend features are disabled."
|
"Plugin license is not configured or invalid. Frontend features are disabled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Plugin-Lizenz ist nicht konfiguriert oder ungültig. Frontend-Funktionen sind "
|
"Plugin-Lizenz ist nicht konfiguriert oder ungültig. Frontend-Funktionen sind "
|
||||||
"deaktiviert."
|
"deaktiviert."
|
||||||
|
|
||||||
#: src/Plugin.php:320
|
#: src/Plugin.php:338
|
||||||
msgid "Configure License"
|
msgid "Configure License"
|
||||||
msgstr "Lizenz konfigurieren"
|
msgstr "Lizenz konfigurieren"
|
||||||
|
|
||||||
@@ -1863,6 +1916,14 @@ msgstr ""
|
|||||||
"WC Licensed Product benötigt WooCommerce als installierte und aktivierte "
|
"WC Licensed Product benötigt WooCommerce als installierte und aktivierte "
|
||||||
"Erweiterung."
|
"Erweiterung."
|
||||||
|
|
||||||
|
#~ msgid "API Verification Secret"
|
||||||
|
#~ msgstr "API-Verifizierungs-Secret"
|
||||||
|
|
||||||
|
#~ msgid "Use this secret to verify signed API responses. Keep it secure."
|
||||||
|
#~ msgstr ""
|
||||||
|
#~ "Verwenden Sie dieses Secret, um signierte API-Antworten zu verifizieren. "
|
||||||
|
#~ "Bewahren Sie es sicher auf."
|
||||||
|
|
||||||
#~ msgid "Domain for License Activation"
|
#~ msgid "Domain for License Activation"
|
||||||
#~ msgstr "Domain für Lizenz-Aktivierung"
|
#~ msgstr "Domain für Lizenz-Aktivierung"
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
# SOME DESCRIPTIVE TITLE.
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
# This file is distributed under the same license as the PACKAGE package.
|
# This file is distributed under the same license as the WC Licensed Product package.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: WC Licensed Product 0.5.3\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
||||||
"POT-Creation-Date: 2026-01-25 18:32+0100\n"
|
"POT-Creation-Date: 2026-01-26 16:08+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -243,7 +243,7 @@ msgstr ""
|
|||||||
#: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200
|
#: src/Admin/AdminController.php:90 src/Admin/AdminController.php:1200
|
||||||
#: src/Admin/OrderLicenseController.php:149
|
#: src/Admin/OrderLicenseController.php:149
|
||||||
#: src/Admin/OrderLicenseController.php:281
|
#: src/Admin/OrderLicenseController.php:281
|
||||||
#: src/Frontend/AccountController.php:90
|
#: src/Frontend/AccountController.php:91
|
||||||
msgid "Licenses"
|
msgid "Licenses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ msgstr ""
|
|||||||
#: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341
|
#: src/Admin/AdminController.php:149 src/Admin/AdminController.php:1341
|
||||||
#: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382
|
#: src/Admin/AdminController.php:1361 src/Admin/AdminController.php:1382
|
||||||
#: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:244
|
#: src/Admin/AdminController.php:1537 src/Admin/OrderLicenseController.php:244
|
||||||
#: src/Frontend/AccountController.php:384
|
#: src/Frontend/AccountController.php:387
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -303,17 +303,19 @@ msgstr ""
|
|||||||
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
|
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
|
||||||
#: src/Admin/DashboardWidgetController.php:136
|
#: src/Admin/DashboardWidgetController.php:136
|
||||||
#: src/Admin/OrderLicenseController.php:260
|
#: src/Admin/OrderLicenseController.php:260
|
||||||
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:110
|
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:136
|
||||||
#: src/Product/LicensedProductType.php:158
|
#: src/Product/LicensedProductType.php:184
|
||||||
#: src/Frontend/AccountController.php:283
|
#: src/Product/LicensedProductType.php:379
|
||||||
|
#: src/Product/LicensedProductVariation.php:139
|
||||||
|
#: src/Frontend/AccountController.php:286
|
||||||
msgid "Lifetime"
|
msgid "Lifetime"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:422
|
#: src/Admin/AdminController.php:152 src/Frontend/AccountController.php:425
|
||||||
msgid "Copied!"
|
msgid "Copied!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:423
|
#: src/Admin/AdminController.php:153 src/Frontend/AccountController.php:426
|
||||||
msgid "Copy failed"
|
msgid "Copy failed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -401,7 +403,7 @@ msgstr ""
|
|||||||
#: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549
|
#: src/Admin/AdminController.php:531 src/Admin/AdminController.php:549
|
||||||
#: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621
|
#: src/Admin/AdminController.php:577 src/Admin/AdminController.php:621
|
||||||
#: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:469
|
#: src/Admin/AdminController.php:811 src/Admin/SettingsController.php:469
|
||||||
#: src/Frontend/AccountController.php:439
|
#: src/Frontend/AccountController.php:442
|
||||||
msgid "Security check failed."
|
msgid "Security check failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -668,8 +670,8 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445
|
#: src/Admin/AdminController.php:1294 src/Admin/AdminController.php:1445
|
||||||
#: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:205
|
#: src/Admin/AdminController.php:1495 src/Admin/OrderLicenseController.php:205
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
#: src/Checkout/CheckoutBlocksIntegration.php:130
|
||||||
#: src/Checkout/CheckoutController.php:122
|
#: src/Checkout/CheckoutController.php:161
|
||||||
#: src/Email/LicenseEmailController.php:288
|
#: src/Email/LicenseEmailController.php:288
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -689,7 +691,7 @@ msgstr ""
|
|||||||
msgid "No licenses found."
|
msgid "No licenses found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:260
|
#: src/Admin/AdminController.php:1314 src/Frontend/AccountController.php:263
|
||||||
msgid "Copy to clipboard"
|
msgid "Copy to clipboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -709,7 +711,7 @@ msgstr ""
|
|||||||
msgid "Test"
|
msgid "Test"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:267
|
#: src/Admin/AdminController.php:1400 src/Frontend/AccountController.php:270
|
||||||
msgid "Transfer to new domain"
|
msgid "Transfer to new domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -737,27 +739,27 @@ msgstr ""
|
|||||||
msgid "Testing license..."
|
msgid "Testing license..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:362
|
#: src/Admin/AdminController.php:1508 src/Frontend/AccountController.php:365
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:363
|
#: src/Admin/AdminController.php:1517 src/Frontend/AccountController.php:366
|
||||||
msgid "Transfer License to New Domain"
|
msgid "Transfer License to New Domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:368
|
#: src/Admin/AdminController.php:1524 src/Frontend/AccountController.php:371
|
||||||
msgid "Current Domain"
|
msgid "Current Domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:373
|
#: src/Admin/AdminController.php:1528 src/Frontend/AccountController.php:376
|
||||||
msgid "New Domain"
|
msgid "New Domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:377
|
#: src/Admin/AdminController.php:1531 src/Frontend/AccountController.php:380
|
||||||
msgid "Enter the new domain without http:// or www."
|
msgid "Enter the new domain without http:// or www."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:382
|
#: src/Admin/AdminController.php:1536 src/Frontend/AccountController.php:385
|
||||||
msgid "Transfer License"
|
msgid "Transfer License"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -940,11 +942,11 @@ msgid "Domains specified during checkout (multi-domain order)."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:119
|
#: src/Admin/OrderLicenseController.php:119
|
||||||
#: src/Checkout/CheckoutController.php:436
|
#: src/Checkout/CheckoutController.php:530
|
||||||
#: src/Checkout/CheckoutController.php:486
|
#: src/Checkout/CheckoutController.php:591
|
||||||
#: src/Checkout/CheckoutController.php:496 src/License/LicenseManager.php:806
|
#: src/Checkout/CheckoutController.php:613 src/License/LicenseManager.php:878
|
||||||
#: src/Product/VersionManager.php:349 src/Product/VersionManager.php:361
|
#: src/Product/VersionManager.php:349 src/Product/VersionManager.php:361
|
||||||
#: src/Frontend/AccountController.php:146
|
#: src/Frontend/AccountController.php:148
|
||||||
#: src/Email/LicenseExpirationEmail.php:107
|
#: src/Email/LicenseExpirationEmail.php:107
|
||||||
#: src/Email/LicenseExpiredEmail.php:99
|
#: src/Email/LicenseExpiredEmail.php:99
|
||||||
msgid "Unknown Product"
|
msgid "Unknown Product"
|
||||||
@@ -957,10 +959,10 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:137
|
#: src/Admin/OrderLicenseController.php:137
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:83
|
#: src/Checkout/CheckoutBlocksIntegration.php:84
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:119
|
#: src/Checkout/CheckoutBlocksIntegration.php:120
|
||||||
#: src/Checkout/CheckoutController.php:130
|
#: src/Checkout/CheckoutController.php:169
|
||||||
#: src/Checkout/CheckoutController.php:186
|
#: src/Checkout/CheckoutController.php:235
|
||||||
msgid "example.com"
|
msgid "example.com"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1019,9 +1021,9 @@ msgid "Error. Please try again."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:373
|
#: src/Admin/OrderLicenseController.php:373
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:126
|
#: src/Checkout/CheckoutBlocksIntegration.php:127
|
||||||
#: src/Frontend/AccountController.php:427
|
#: src/Frontend/AccountController.php:430
|
||||||
#: src/Frontend/AccountController.php:459
|
#: src/Frontend/AccountController.php:462
|
||||||
msgid "Please enter a valid domain."
|
msgid "Please enter a valid domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1045,7 +1047,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: src/Admin/OrderLicenseController.php:449
|
#: src/Admin/OrderLicenseController.php:449
|
||||||
#: src/Frontend/DownloadController.php:117
|
#: src/Frontend/DownloadController.php:117
|
||||||
#: src/Frontend/AccountController.php:465
|
#: src/Frontend/AccountController.php:468
|
||||||
msgid "License not found."
|
msgid "License not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1241,7 +1243,7 @@ msgid "Too many requests. Please try again later."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
|
#: src/Api/RestApiController.php:345 src/Api/RestApiController.php:378
|
||||||
#: src/License/LicenseManager.php:403
|
#: src/License/LicenseManager.php:475
|
||||||
msgid "License key not found."
|
msgid "License key not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1265,89 +1267,95 @@ msgstr ""
|
|||||||
msgid "License activated successfully."
|
msgid "License activated successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:78
|
#: src/Checkout/CheckoutBlocksIntegration.php:79
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:125
|
#: src/Checkout/CheckoutBlocksIntegration.php:126
|
||||||
#: src/Checkout/CheckoutController.php:119
|
#: src/Checkout/CheckoutController.php:158
|
||||||
msgid "License Domain"
|
msgid "License Domain"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:85
|
#: src/Checkout/CheckoutBlocksIntegration.php:86
|
||||||
msgid "Enter a valid domain (without http:// or www)"
|
msgid "Enter a valid domain (without http:// or www)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:121
|
#: src/Checkout/CheckoutBlocksIntegration.php:122
|
||||||
#: src/Checkout/CheckoutController.php:150
|
#: src/Checkout/CheckoutController.php:189
|
||||||
msgid "Enter a unique domain for each license (without http:// or www)."
|
msgid "Enter a unique domain for each license (without http:// or www)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:122
|
#: src/Checkout/CheckoutBlocksIntegration.php:123
|
||||||
#: src/Checkout/CheckoutController.php:134
|
#: src/Checkout/CheckoutController.php:173
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter the domain where you will use the license (without http:// or www)."
|
"Enter the domain where you will use the license (without http:// or www)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:124
|
#: src/Checkout/CheckoutBlocksIntegration.php:125
|
||||||
#: src/Checkout/CheckoutController.php:148
|
#: src/Checkout/CheckoutController.php:187
|
||||||
msgid "License Domains"
|
msgid "License Domains"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:127
|
#: src/Checkout/CheckoutBlocksIntegration.php:128
|
||||||
msgid "Each license requires a unique domain."
|
msgid "Each license requires a unique domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:128
|
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
||||||
#: src/Checkout/CheckoutController.php:175
|
#: src/Checkout/CheckoutController.php:224
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "License %d:"
|
msgid "License %d:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:123
|
#: src/Checkout/CheckoutController.php:162
|
||||||
#: src/Checkout/CheckoutController.php:179
|
#: src/Checkout/CheckoutController.php:228
|
||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:258
|
#: src/Checkout/CheckoutController.php:323
|
||||||
msgid "Please enter a domain for your license."
|
msgid "Please enter a domain for your license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:264
|
#: src/Checkout/CheckoutController.php:329
|
||||||
msgid "Please enter a valid domain for your license."
|
msgid "Please enter a valid domain for your license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:287
|
#: src/Checkout/CheckoutController.php:356
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Please enter a domain for %1$s (License %2$d)."
|
msgid "Please enter a domain for %1$s (License %2$d)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:302
|
#: src/Checkout/CheckoutController.php:371
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Please enter a valid domain for %1$s (License %2$d)."
|
msgid "Please enter a valid domain for %1$s (License %2$d)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:316
|
#: src/Checkout/CheckoutController.php:385
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The domain \"%1$s\" is used multiple times for %2$s. Each license requires a "
|
"The domain \"%1$s\" is used multiple times for %2$s. Each license requires a "
|
||||||
"unique domain."
|
"unique domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:419
|
#: src/Checkout/CheckoutController.php:500
|
||||||
#: src/Checkout/CheckoutController.php:466
|
#: src/Checkout/CheckoutController.php:561
|
||||||
#: src/Checkout/CheckoutController.php:470
|
#: src/Checkout/CheckoutController.php:565
|
||||||
msgid "License Domain:"
|
msgid "License Domain:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:432
|
#: src/Checkout/CheckoutController.php:513
|
||||||
#: src/Checkout/CheckoutController.php:483
|
#: src/Checkout/CheckoutController.php:578
|
||||||
#: src/Checkout/CheckoutController.php:492
|
#: src/Checkout/CheckoutController.php:599
|
||||||
msgid "License Domains:"
|
msgid "License Domains:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Checkout/CheckoutController.php:522
|
||||||
|
#: src/Checkout/CheckoutController.php:585
|
||||||
|
#: src/Checkout/CheckoutController.php:607
|
||||||
|
msgid "Unknown Variation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/StoreApiExtension.php:93
|
#: src/Checkout/StoreApiExtension.php:93
|
||||||
msgid "Domains for license activation by product"
|
msgid "Domains for license activation by product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/StoreApiExtension.php:117
|
#: src/Checkout/StoreApiExtension.php:120
|
||||||
msgid "Domain for license activation"
|
msgid "Domain for license activation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1359,85 +1367,111 @@ msgstr ""
|
|||||||
msgid "Could not connect to license server."
|
msgid "Could not connect to license server."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:412
|
#: src/License/LicenseManager.php:484
|
||||||
msgid "This license has been revoked."
|
msgid "This license has been revoked."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:422
|
#: src/License/LicenseManager.php:494
|
||||||
msgid "This license has expired."
|
msgid "This license has expired."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:430
|
#: src/License/LicenseManager.php:502
|
||||||
msgid "This license is inactive."
|
msgid "This license is inactive."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/License/LicenseManager.php:440
|
#: src/License/LicenseManager.php:512
|
||||||
msgid "This license is not valid for this domain."
|
msgid "This license is not valid for this domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:61
|
#: src/Product/LicensedProductType.php:72
|
||||||
msgid "Licensed Product"
|
msgid "Licensed Product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:82
|
#: src/Product/LicensedProductType.php:73
|
||||||
|
msgid "Licensed Variable Product"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:108
|
||||||
msgid "License Settings"
|
msgid "License Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:109
|
#: src/Product/LicensedProductType.php:135
|
||||||
|
#: src/Product/LicensedProductType.php:378
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d days"
|
msgid "%d days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:119
|
#: src/Product/LicensedProductType.php:145
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Leave fields empty to use default settings from %s."
|
msgid "Leave fields empty to use default settings from %s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:121
|
#: src/Product/LicensedProductType.php:147
|
||||||
msgid "WooCommerce > Settings > Licensed Products"
|
msgid "WooCommerce > Settings > Licensed Products"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:128
|
#: src/Product/LicensedProductType.php:154
|
||||||
|
#: src/Product/LicensedProductType.php:396
|
||||||
msgid "Max Activations"
|
msgid "Max Activations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:131
|
#: src/Product/LicensedProductType.php:157
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Maximum number of domain activations per license. Default: %d"
|
msgid "Maximum number of domain activations per license. Default: %d"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:146
|
#: src/Product/LicensedProductType.php:172
|
||||||
msgid "License Validity (Days)"
|
msgid "License Validity (Days)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:149
|
#: src/Product/LicensedProductType.php:175
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Number of days the license is valid. Leave empty for default (%s)."
|
msgid "Number of days the license is valid. Leave empty for default (%s)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:164
|
#: src/Product/LicensedProductType.php:190
|
||||||
msgid "Bind to Major Version"
|
msgid "Bind to Major Version"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:167
|
#: src/Product/LicensedProductType.php:193
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"If enabled, licenses are bound to the major version at purchase time. "
|
"If enabled, licenses are bound to the major version at purchase time. "
|
||||||
"Default: %s"
|
"Default: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:168
|
#: src/Product/LicensedProductType.php:194
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:168
|
#: src/Product/LicensedProductType.php:194
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:288
|
#: src/Product/LicensedProductType.php:321
|
||||||
msgid "Version:"
|
msgid "Version:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:349
|
||||||
|
msgid "Licensed products are always virtual"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:351
|
||||||
|
msgid "Virtual"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:384
|
||||||
|
msgid "License Duration (Days)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:393
|
||||||
|
msgid "Leave empty for parent default. 0 = Lifetime."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductType.php:405
|
||||||
|
msgid "Leave empty for parent default."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/VersionManager.php:166
|
#: src/Product/VersionManager.php:166
|
||||||
msgid "Attachment file not found."
|
msgid "Attachment file not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1447,6 +1481,25 @@ msgstr ""
|
|||||||
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
msgid "File checksum does not match. Expected: %1$s, Got: %2$s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:143
|
||||||
|
msgid "Monthly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:147
|
||||||
|
msgid "Quarterly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:151
|
||||||
|
msgid "Yearly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Product/LicensedProductVariation.php:156
|
||||||
|
#, php-format
|
||||||
|
msgid "%d day"
|
||||||
|
msgid_plural "%d days"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
#: src/Frontend/DownloadController.php:77
|
#: src/Frontend/DownloadController.php:77
|
||||||
#: src/Frontend/DownloadController.php:101
|
#: src/Frontend/DownloadController.php:101
|
||||||
msgid "Invalid download link."
|
msgid "Invalid download link."
|
||||||
@@ -1498,78 +1551,78 @@ msgstr ""
|
|||||||
msgid "Download file not found."
|
msgid "Download file not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:104
|
#: src/Frontend/AccountController.php:105
|
||||||
msgid "Please log in to view your licenses."
|
msgid "Please log in to view your licenses."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:220
|
#: src/Frontend/AccountController.php:223
|
||||||
msgid "You have no licenses yet."
|
msgid "You have no licenses yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:242
|
#: src/Frontend/AccountController.php:245
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Order #%s"
|
msgid "Order #%s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:293
|
#: src/Frontend/AccountController.php:296
|
||||||
msgid "Available Downloads"
|
msgid "Available Downloads"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:302
|
#: src/Frontend/AccountController.php:305
|
||||||
#: src/Frontend/AccountController.php:335
|
#: src/Frontend/AccountController.php:338
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Version %s"
|
msgid "Version %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:304
|
#: src/Frontend/AccountController.php:307
|
||||||
msgid "Latest"
|
msgid "Latest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:324
|
#: src/Frontend/AccountController.php:327
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Older versions (%d)"
|
msgid "Older versions (%d)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:424
|
#: src/Frontend/AccountController.php:427
|
||||||
#: src/Frontend/AccountController.php:491
|
#: src/Frontend/AccountController.php:494
|
||||||
msgid "License transferred successfully!"
|
msgid "License transferred successfully!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:425
|
#: src/Frontend/AccountController.php:428
|
||||||
msgid "Transfer failed. Please try again."
|
msgid "Transfer failed. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:426
|
#: src/Frontend/AccountController.php:429
|
||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure you want to transfer this license to a new domain? This action "
|
"Are you sure you want to transfer this license to a new domain? This action "
|
||||||
"cannot be undone."
|
"cannot be undone."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:445
|
#: src/Frontend/AccountController.php:448
|
||||||
msgid "Please log in to transfer a license."
|
msgid "Please log in to transfer a license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:451
|
#: src/Frontend/AccountController.php:454
|
||||||
msgid "Invalid license."
|
msgid "Invalid license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:469
|
#: src/Frontend/AccountController.php:472
|
||||||
msgid "You do not have permission to transfer this license."
|
msgid "You do not have permission to transfer this license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:474
|
#: src/Frontend/AccountController.php:477
|
||||||
msgid "Revoked licenses cannot be transferred."
|
msgid "Revoked licenses cannot be transferred."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:478
|
#: src/Frontend/AccountController.php:481
|
||||||
msgid "Expired licenses cannot be transferred."
|
msgid "Expired licenses cannot be transferred."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:483
|
#: src/Frontend/AccountController.php:486
|
||||||
msgid "The new domain is the same as the current domain."
|
msgid "The new domain is the same as the current domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Frontend/AccountController.php:495
|
#: src/Frontend/AccountController.php:498
|
||||||
msgid "Failed to transfer license. Please try again."
|
msgid "Failed to transfer license. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1772,16 +1825,16 @@ msgstr ""
|
|||||||
msgid "YOUR LICENSE KEYS"
|
msgid "YOUR LICENSE KEYS"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Plugin.php:318
|
#: src/Plugin.php:336
|
||||||
msgid "WC Licensed Product"
|
msgid "WC Licensed Product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Plugin.php:319
|
#: src/Plugin.php:337
|
||||||
msgid ""
|
msgid ""
|
||||||
"Plugin license is not configured or invalid. Frontend features are disabled."
|
"Plugin license is not configured or invalid. Frontend features are disabled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Plugin.php:320
|
#: src/Plugin.php:338
|
||||||
msgid "Configure License"
|
msgid "Configure License"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -147,9 +147,52 @@ final class ResponseSigner
|
|||||||
*/
|
*/
|
||||||
private function deriveKey(string $licenseKey): string
|
private function deriveKey(string $licenseKey): string
|
||||||
{
|
{
|
||||||
// HKDF-like key derivation
|
return self::deriveCustomerSecret($licenseKey, $this->serverSecret);
|
||||||
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
|
}
|
||||||
|
|
||||||
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
|
/**
|
||||||
|
* Derive a customer-specific secret from a license key
|
||||||
|
*
|
||||||
|
* This secret is unique per license and can be shared with the customer
|
||||||
|
* to verify signed API responses. Each customer gets their own secret
|
||||||
|
* derived from their license key.
|
||||||
|
*
|
||||||
|
* @param string $licenseKey The customer's license key
|
||||||
|
* @param string $serverSecret The server's master secret
|
||||||
|
* @return string The derived secret (64 hex characters)
|
||||||
|
*/
|
||||||
|
public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
|
||||||
|
{
|
||||||
|
// HKDF-like key derivation
|
||||||
|
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
|
||||||
|
|
||||||
|
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the customer secret for a license key using the configured server secret
|
||||||
|
*
|
||||||
|
* @param string $licenseKey The customer's license key
|
||||||
|
* @return string|null The derived secret, or null if server secret is not configured
|
||||||
|
*/
|
||||||
|
public static function getCustomerSecretForLicense(string $licenseKey): ?string
|
||||||
|
{
|
||||||
|
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : '';
|
||||||
|
|
||||||
|
if (empty($serverSecret)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::deriveCustomerSecret($licenseKey, $serverSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if response signing is enabled
|
||||||
|
*
|
||||||
|
* @return bool True if server secret is configured
|
||||||
|
*/
|
||||||
|
public static function isSigningEnabled(): bool
|
||||||
|
{
|
||||||
|
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Jeremias\WcLicensedProduct\Checkout;
|
|||||||
|
|
||||||
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
|
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
|
||||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
|
use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration with WooCommerce Checkout Blocks
|
* Integration with WooCommerce Checkout Blocks
|
||||||
@@ -141,7 +142,7 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
|||||||
/**
|
/**
|
||||||
* Get licensed products from cart with quantities
|
* Get licensed products from cart with quantities
|
||||||
*
|
*
|
||||||
* @return array<int, array{product_id: int, name: string, quantity: int}>
|
* @return array<string, array{product_id: int, variation_id: int, name: string, quantity: int, duration_label: string}>
|
||||||
*/
|
*/
|
||||||
private function getLicensedProductsFromCart(): array
|
private function getLicensedProductsFromCart(): array
|
||||||
{
|
{
|
||||||
@@ -152,13 +153,49 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
|||||||
$licensedProducts = [];
|
$licensedProducts = [];
|
||||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||||
$product = $cartItem['data'];
|
$product = $cartItem['data'];
|
||||||
if ($product && $product->is_type('licensed')) {
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for simple licensed products
|
||||||
|
if ($product->is_type('licensed')) {
|
||||||
$productId = $product->get_id();
|
$productId = $product->get_id();
|
||||||
$licensedProducts[] = [
|
$licensedProducts[] = [
|
||||||
'product_id' => $productId,
|
'product_id' => $productId,
|
||||||
|
'variation_id' => 0,
|
||||||
'name' => $product->get_name(),
|
'name' => $product->get_name(),
|
||||||
'quantity' => (int) $cartItem['quantity'],
|
'quantity' => (int) $cartItem['quantity'],
|
||||||
|
'duration_label' => '',
|
||||||
];
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for variations of licensed-variable products
|
||||||
|
if ($product->is_type('variation')) {
|
||||||
|
$parentId = $product->get_parent_id();
|
||||||
|
$parent = wc_get_product($parentId);
|
||||||
|
|
||||||
|
if ($parent && $parent->is_type('licensed-variable')) {
|
||||||
|
$variationId = $product->get_id();
|
||||||
|
|
||||||
|
// Get duration label if it's a LicensedProductVariation
|
||||||
|
$durationLabel = '';
|
||||||
|
if ($product instanceof LicensedProductVariation) {
|
||||||
|
$durationLabel = $product->get_license_duration_label();
|
||||||
|
} else {
|
||||||
|
// Try to instantiate as LicensedProductVariation
|
||||||
|
$variation = new LicensedProductVariation($variationId);
|
||||||
|
$durationLabel = $variation->get_license_duration_label();
|
||||||
|
}
|
||||||
|
|
||||||
|
$licensedProducts[] = [
|
||||||
|
'product_id' => $parentId,
|
||||||
|
'variation_id' => $variationId,
|
||||||
|
'name' => $product->get_name(),
|
||||||
|
'quantity' => (int) $cartItem['quantity'],
|
||||||
|
'duration_label' => $durationLabel,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace Jeremias\WcLicensedProduct\Checkout;
|
|||||||
|
|
||||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
|
use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles checkout modifications for licensed products
|
* Handles checkout modifications for licensed products
|
||||||
@@ -57,7 +58,7 @@ final class CheckoutController
|
|||||||
/**
|
/**
|
||||||
* Get licensed products from cart with quantities
|
* Get licensed products from cart with quantities
|
||||||
*
|
*
|
||||||
* @return array<int, array{product_id: int, name: string, quantity: int}>
|
* @return array<string, array{product_id: int, variation_id: int, name: string, quantity: int, duration_label: string}>
|
||||||
*/
|
*/
|
||||||
private function getLicensedProductsFromCart(): array
|
private function getLicensedProductsFromCart(): array
|
||||||
{
|
{
|
||||||
@@ -68,13 +69,51 @@ final class CheckoutController
|
|||||||
$licensedProducts = [];
|
$licensedProducts = [];
|
||||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
foreach (WC()->cart->get_cart() as $cartItem) {
|
||||||
$product = $cartItem['data'];
|
$product = $cartItem['data'];
|
||||||
if ($product && $product->is_type('licensed')) {
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for simple licensed products
|
||||||
|
if ($product->is_type('licensed')) {
|
||||||
$productId = $product->get_id();
|
$productId = $product->get_id();
|
||||||
$licensedProducts[$productId] = [
|
$licensedProducts[$productId] = [
|
||||||
'product_id' => $productId,
|
'product_id' => $productId,
|
||||||
|
'variation_id' => 0,
|
||||||
'name' => $product->get_name(),
|
'name' => $product->get_name(),
|
||||||
'quantity' => (int) $cartItem['quantity'],
|
'quantity' => (int) $cartItem['quantity'],
|
||||||
|
'duration_label' => '',
|
||||||
];
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for variations of licensed-variable products
|
||||||
|
if ($product->is_type('variation')) {
|
||||||
|
$parentId = $product->get_parent_id();
|
||||||
|
$parent = wc_get_product($parentId);
|
||||||
|
|
||||||
|
if ($parent && $parent->is_type('licensed-variable')) {
|
||||||
|
$variationId = $product->get_id();
|
||||||
|
// Use combination key to allow same product with different variations
|
||||||
|
$key = "{$parentId}_{$variationId}";
|
||||||
|
|
||||||
|
// Get duration label if it's a LicensedProductVariation
|
||||||
|
$durationLabel = '';
|
||||||
|
if ($product instanceof LicensedProductVariation) {
|
||||||
|
$durationLabel = $product->get_license_duration_label();
|
||||||
|
} else {
|
||||||
|
// Try to instantiate as LicensedProductVariation
|
||||||
|
$variation = new LicensedProductVariation($variationId);
|
||||||
|
$durationLabel = $variation->get_license_duration_label();
|
||||||
|
}
|
||||||
|
|
||||||
|
$licensedProducts[$key] = [
|
||||||
|
'product_id' => $parentId,
|
||||||
|
'variation_id' => $variationId,
|
||||||
|
'name' => $product->get_name(),
|
||||||
|
'quantity' => (int) $cartItem['quantity'],
|
||||||
|
'duration_label' => $durationLabel,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,22 +189,32 @@ final class CheckoutController
|
|||||||
<?php esc_html_e('Enter a unique domain for each license (without http:// or www).', 'wc-licensed-product'); ?>
|
<?php esc_html_e('Enter a unique domain for each license (without http:// or www).', 'wc-licensed-product'); ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<?php foreach ($licensedProducts as $productId => $productData): ?>
|
<?php foreach ($licensedProducts as $key => $productData): ?>
|
||||||
<div class="wclp-product-domains" data-product-id="<?php echo esc_attr($productId); ?>">
|
<?php
|
||||||
|
$productId = $productData['product_id'];
|
||||||
|
$variationId = $productData['variation_id'] ?? 0;
|
||||||
|
$durationLabel = $productData['duration_label'] ?? '';
|
||||||
|
// Use key for field names to handle variations
|
||||||
|
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : $productId;
|
||||||
|
?>
|
||||||
|
<div class="wclp-product-domains" data-product-id="<?php echo esc_attr($productId); ?>" data-variation-id="<?php echo esc_attr($variationId); ?>">
|
||||||
<h4>
|
<h4>
|
||||||
<?php
|
<?php
|
||||||
echo esc_html($productData['name']);
|
echo esc_html($productData['name']);
|
||||||
|
if (!empty($durationLabel)) {
|
||||||
|
echo ' <span class="wclp-duration-badge">(' . esc_html($durationLabel) . ')</span>';
|
||||||
|
}
|
||||||
if ($productData['quantity'] > 1) {
|
if ($productData['quantity'] > 1) {
|
||||||
printf(' (×%d)', $productData['quantity']);
|
printf(' ×%d', $productData['quantity']);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<?php for ($i = 0; $i < $productData['quantity']; $i++): ?>
|
<?php for ($i = 0; $i < $productData['quantity']; $i++): ?>
|
||||||
<?php
|
<?php
|
||||||
$fieldName = sprintf('licensed_domains[%d][%d]', $productId, $i);
|
$fieldName = sprintf('licensed_domains[%s][%d]', $fieldKey, $i);
|
||||||
$fieldId = sprintf('licensed_domain_%d_%d', $productId, $i);
|
$fieldId = sprintf('licensed_domain_%s_%d', str_replace('_', '-', $fieldKey), $i);
|
||||||
$savedValue = $this->getSavedDomainValue($productId, $i);
|
$savedValue = $this->getSavedDomainValue($productId, $i, $variationId);
|
||||||
?>
|
?>
|
||||||
<p class="form-row form-row-wide wclp-domain-row">
|
<p class="form-row form-row-wide wclp-domain-row">
|
||||||
<label for="<?php echo esc_attr($fieldId); ?>">
|
<label for="<?php echo esc_attr($fieldId); ?>">
|
||||||
@@ -186,6 +235,9 @@ final class CheckoutController
|
|||||||
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
|
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>"
|
||||||
value="<?php echo esc_attr($savedValue); ?>"
|
value="<?php echo esc_attr($savedValue); ?>"
|
||||||
/>
|
/>
|
||||||
|
<?php if ($variationId > 0): ?>
|
||||||
|
<input type="hidden" name="licensed_variation_ids[<?php echo esc_attr($fieldKey); ?>]" value="<?php echo esc_attr($variationId); ?>" />
|
||||||
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
<?php endfor; ?>
|
<?php endfor; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,6 +249,7 @@ final class CheckoutController
|
|||||||
.wclp-domain-description { margin-bottom: 15px; color: #666; }
|
.wclp-domain-description { margin-bottom: 15px; color: #666; }
|
||||||
.wclp-product-domains { margin-bottom: 20px; padding: 15px; background: #f8f8f8; border-radius: 4px; }
|
.wclp-product-domains { margin-bottom: 20px; padding: 15px; background: #f8f8f8; border-radius: 4px; }
|
||||||
.wclp-product-domains h4 { margin: 0 0 10px 0; font-size: 1em; }
|
.wclp-product-domains h4 { margin: 0 0 10px 0; font-size: 1em; }
|
||||||
|
.wclp-duration-badge { color: #0073aa; font-weight: normal; }
|
||||||
.wclp-domain-row { margin-bottom: 10px; }
|
.wclp-domain-row { margin-bottom: 10px; }
|
||||||
.wclp-domain-row:last-child { margin-bottom: 0; }
|
.wclp-domain-row:last-child { margin-bottom: 0; }
|
||||||
.wclp-domain-row label { display: block; margin-bottom: 5px; }
|
.wclp-domain-row label { display: block; margin-bottom: 5px; }
|
||||||
@@ -207,9 +260,17 @@ final class CheckoutController
|
|||||||
/**
|
/**
|
||||||
* Get saved domain value from session/POST
|
* Get saved domain value from session/POST
|
||||||
*/
|
*/
|
||||||
private function getSavedDomainValue(int $productId, int $index): string
|
private function getSavedDomainValue(int $productId, int $index, int $variationId = 0): string
|
||||||
{
|
{
|
||||||
|
// Build the field key (with or without variation)
|
||||||
|
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||||
|
|
||||||
// Check POST data first (validation failure case)
|
// Check POST data first (validation failure case)
|
||||||
|
if (isset($_POST['licensed_domains'][$fieldKey][$index])) {
|
||||||
|
return sanitize_text_field($_POST['licensed_domains'][$fieldKey][$index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try numeric key for backward compatibility
|
||||||
if (isset($_POST['licensed_domains'][$productId][$index])) {
|
if (isset($_POST['licensed_domains'][$productId][$index])) {
|
||||||
return sanitize_text_field($_POST['licensed_domains'][$productId][$index]);
|
return sanitize_text_field($_POST['licensed_domains'][$productId][$index]);
|
||||||
}
|
}
|
||||||
@@ -218,7 +279,11 @@ final class CheckoutController
|
|||||||
if (WC()->session) {
|
if (WC()->session) {
|
||||||
$sessionDomains = WC()->session->get('licensed_product_domains', []);
|
$sessionDomains = WC()->session->get('licensed_product_domains', []);
|
||||||
foreach ($sessionDomains as $item) {
|
foreach ($sessionDomains as $item) {
|
||||||
if (isset($item['product_id']) && (int) $item['product_id'] === $productId) {
|
$itemProductId = (int) ($item['product_id'] ?? 0);
|
||||||
|
$itemVariationId = (int) ($item['variation_id'] ?? 0);
|
||||||
|
|
||||||
|
// Match by product and variation
|
||||||
|
if ($itemProductId === $productId && $itemVariationId === $variationId) {
|
||||||
if (isset($item['domains'][$index])) {
|
if (isset($item['domains'][$index])) {
|
||||||
return $item['domains'][$index];
|
return $item['domains'][$index];
|
||||||
}
|
}
|
||||||
@@ -272,8 +337,12 @@ final class CheckoutController
|
|||||||
{
|
{
|
||||||
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
||||||
|
|
||||||
foreach ($licensedProducts as $productId => $productData) {
|
foreach ($licensedProducts as $key => $productData) {
|
||||||
$productDomains = $licensedDomains[$productId] ?? [];
|
$productId = $productData['product_id'];
|
||||||
|
$variationId = $productData['variation_id'] ?? 0;
|
||||||
|
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||||
|
|
||||||
|
$productDomains = $licensedDomains[$fieldKey] ?? $licensedDomains[$productId] ?? [];
|
||||||
$normalizedDomains = [];
|
$normalizedDomains = [];
|
||||||
|
|
||||||
for ($i = 0; $i < $productData['quantity']; $i++) {
|
for ($i = 0; $i < $productData['quantity']; $i++) {
|
||||||
@@ -308,7 +377,7 @@ final class CheckoutController
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate domains within same product
|
// Check for duplicate domains within same product/variation
|
||||||
if (in_array($normalizedDomain, $normalizedDomains, true)) {
|
if (in_array($normalizedDomain, $normalizedDomains, true)) {
|
||||||
wc_add_notice(
|
wc_add_notice(
|
||||||
sprintf(
|
sprintf(
|
||||||
@@ -369,10 +438,15 @@ final class CheckoutController
|
|||||||
private function saveMultiDomainFields(\WC_Order $order, array $licensedProducts): void
|
private function saveMultiDomainFields(\WC_Order $order, array $licensedProducts): void
|
||||||
{
|
{
|
||||||
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
$licensedDomains = $_POST['licensed_domains'] ?? [];
|
||||||
|
$licensedVariationIds = $_POST['licensed_variation_ids'] ?? [];
|
||||||
$domainData = [];
|
$domainData = [];
|
||||||
|
|
||||||
foreach ($licensedProducts as $productId => $productData) {
|
foreach ($licensedProducts as $key => $productData) {
|
||||||
$productDomains = $licensedDomains[$productId] ?? [];
|
$productId = $productData['product_id'];
|
||||||
|
$variationId = $productData['variation_id'] ?? 0;
|
||||||
|
$fieldKey = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||||
|
|
||||||
|
$productDomains = $licensedDomains[$fieldKey] ?? $licensedDomains[$productId] ?? [];
|
||||||
$normalizedDomains = [];
|
$normalizedDomains = [];
|
||||||
|
|
||||||
for ($i = 0; $i < $productData['quantity']; $i++) {
|
for ($i = 0; $i < $productData['quantity']; $i++) {
|
||||||
@@ -383,10 +457,17 @@ final class CheckoutController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($normalizedDomains)) {
|
if (!empty($normalizedDomains)) {
|
||||||
$domainData[] = [
|
$entry = [
|
||||||
'product_id' => $productId,
|
'product_id' => $productId,
|
||||||
'domains' => $normalizedDomains,
|
'domains' => $normalizedDomains,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Include variation_id if present
|
||||||
|
if ($variationId > 0) {
|
||||||
|
$entry['variation_id'] = $variationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$domainData[] = $entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,8 +513,22 @@ final class CheckoutController
|
|||||||
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
|
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
|
||||||
<?php foreach ($domainData as $item): ?>
|
<?php foreach ($domainData as $item): ?>
|
||||||
<?php
|
<?php
|
||||||
$product = wc_get_product($item['product_id']);
|
$productId = $item['product_id'];
|
||||||
|
$variationId = $item['variation_id'] ?? 0;
|
||||||
|
|
||||||
|
// Get product name
|
||||||
|
if ($variationId > 0) {
|
||||||
|
$variation = wc_get_product($variationId);
|
||||||
|
$productName = $variation ? $variation->get_name() : __('Unknown Variation', 'wc-licensed-product');
|
||||||
|
|
||||||
|
// Add duration label if available
|
||||||
|
if ($variation instanceof LicensedProductVariation) {
|
||||||
|
$productName .= ' (' . $variation->get_license_duration_label() . ')';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$product = wc_get_product($productId);
|
||||||
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<p style="margin: 5px 0 5px 15px;">
|
<p style="margin: 5px 0 5px 15px;">
|
||||||
<em><?php echo esc_html($productName); ?>:</em><br>
|
<em><?php echo esc_html($productName); ?>:</em><br>
|
||||||
@@ -482,8 +577,20 @@ final class CheckoutController
|
|||||||
if ($plainText) {
|
if ($plainText) {
|
||||||
echo "\n" . esc_html__('License Domains:', 'wc-licensed-product') . "\n";
|
echo "\n" . esc_html__('License Domains:', 'wc-licensed-product') . "\n";
|
||||||
foreach ($domainData as $item) {
|
foreach ($domainData as $item) {
|
||||||
$product = wc_get_product($item['product_id']);
|
$productId = $item['product_id'];
|
||||||
|
$variationId = $item['variation_id'] ?? 0;
|
||||||
|
|
||||||
|
if ($variationId > 0) {
|
||||||
|
$variation = wc_get_product($variationId);
|
||||||
|
$productName = $variation ? $variation->get_name() : __('Unknown Variation', 'wc-licensed-product');
|
||||||
|
if ($variation instanceof LicensedProductVariation) {
|
||||||
|
$productName .= ' (' . $variation->get_license_duration_label() . ')';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$product = wc_get_product($productId);
|
||||||
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
echo ' ' . esc_html($productName) . ': ' . esc_html(implode(', ', $item['domains'])) . "\n";
|
echo ' ' . esc_html($productName) . ': ' . esc_html(implode(', ', $item['domains'])) . "\n";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -492,8 +599,19 @@ final class CheckoutController
|
|||||||
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
|
<strong><?php esc_html_e('License Domains:', 'wc-licensed-product'); ?></strong>
|
||||||
<?php foreach ($domainData as $item): ?>
|
<?php foreach ($domainData as $item): ?>
|
||||||
<?php
|
<?php
|
||||||
$product = wc_get_product($item['product_id']);
|
$productId = $item['product_id'];
|
||||||
|
$variationId = $item['variation_id'] ?? 0;
|
||||||
|
|
||||||
|
if ($variationId > 0) {
|
||||||
|
$variation = wc_get_product($variationId);
|
||||||
|
$productName = $variation ? $variation->get_name() : __('Unknown Variation', 'wc-licensed-product');
|
||||||
|
if ($variation instanceof LicensedProductVariation) {
|
||||||
|
$productName .= ' (' . $variation->get_license_duration_label() . ')';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$product = wc_get_product($productId);
|
||||||
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<p style="margin: 5px 0 5px 15px;">
|
<p style="margin: 5px 0 5px 15px;">
|
||||||
<em><?php echo esc_html($productName); ?>:</em><br>
|
<em><?php echo esc_html($productName); ?>:</em><br>
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ final class StoreApiExtension
|
|||||||
'product_id' => [
|
'product_id' => [
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
],
|
],
|
||||||
|
'variation_id' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
],
|
||||||
'domains' => [
|
'domains' => [
|
||||||
'type' => 'array',
|
'type' => 'array',
|
||||||
'items' => [
|
'items' => [
|
||||||
@@ -162,6 +165,7 @@ final class StoreApiExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
$productId = (int) $item['product_id'];
|
$productId = (int) $item['product_id'];
|
||||||
|
$variationId = isset($item['variation_id']) ? (int) $item['variation_id'] : 0;
|
||||||
$domains = [];
|
$domains = [];
|
||||||
|
|
||||||
foreach ($item['domains'] as $domain) {
|
foreach ($item['domains'] as $domain) {
|
||||||
@@ -172,10 +176,17 @@ final class StoreApiExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($domains)) {
|
if (!empty($domains)) {
|
||||||
$normalized[] = [
|
$entry = [
|
||||||
'product_id' => $productId,
|
'product_id' => $productId,
|
||||||
'domains' => $domains,
|
'domains' => $domains,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Include variation_id if present
|
||||||
|
if ($variationId > 0) {
|
||||||
|
$entry['variation_id'] = $variationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized[] = $entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,10 +278,23 @@ final class StoreApiExtension
|
|||||||
// Check for licensed_domains in classic format (from DOM injection)
|
// Check for licensed_domains in classic format (from DOM injection)
|
||||||
if (empty($domainData) && isset($requestData['licensed_domains']) && is_array($requestData['licensed_domains'])) {
|
if (empty($domainData) && isset($requestData['licensed_domains']) && is_array($requestData['licensed_domains'])) {
|
||||||
$domainData = [];
|
$domainData = [];
|
||||||
foreach ($requestData['licensed_domains'] as $productId => $domains) {
|
$variationIds = $requestData['licensed_variation_ids'] ?? [];
|
||||||
|
|
||||||
|
foreach ($requestData['licensed_domains'] as $key => $domains) {
|
||||||
if (!is_array($domains)) {
|
if (!is_array($domains)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse key - could be "productId" or "productId_variationId"
|
||||||
|
$parts = explode('_', (string) $key);
|
||||||
|
$productId = (int) $parts[0];
|
||||||
|
$variationId = isset($parts[1]) ? (int) $parts[1] : 0;
|
||||||
|
|
||||||
|
// Also check for hidden variation ID field
|
||||||
|
if ($variationId === 0 && isset($variationIds[$key])) {
|
||||||
|
$variationId = (int) $variationIds[$key];
|
||||||
|
}
|
||||||
|
|
||||||
$normalizedDomains = [];
|
$normalizedDomains = [];
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
$sanitized = sanitize_text_field($domain);
|
$sanitized = sanitize_text_field($domain);
|
||||||
@@ -279,10 +303,16 @@ final class StoreApiExtension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty($normalizedDomains)) {
|
if (!empty($normalizedDomains)) {
|
||||||
$domainData[] = [
|
$entry = [
|
||||||
'product_id' => (int) $productId,
|
'product_id' => $productId,
|
||||||
'domains' => $normalizedDomains,
|
'domains' => $normalizedDomains,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($variationId > 0) {
|
||||||
|
$entry['variation_id'] = $variationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$domainData[] = $entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Jeremias\WcLicensedProduct\Frontend;
|
namespace Jeremias\WcLicensedProduct\Frontend;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
|
||||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -114,6 +115,7 @@ final class AccountController
|
|||||||
echo $this->twig->render('frontend/licenses.html.twig', [
|
echo $this->twig->render('frontend/licenses.html.twig', [
|
||||||
'packages' => $packages,
|
'packages' => $packages,
|
||||||
'has_packages' => !empty($packages),
|
'has_packages' => !empty($packages),
|
||||||
|
'signing_enabled' => ResponseSigner::isSigningEnabled(),
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Fallback to PHP template if Twig fails
|
// Fallback to PHP template if Twig fails
|
||||||
@@ -161,6 +163,7 @@ final class AccountController
|
|||||||
'status' => $license->getStatus(),
|
'status' => $license->getStatus(),
|
||||||
'expires_at' => $license->getExpiresAt(),
|
'expires_at' => $license->getExpiresAt(),
|
||||||
'is_transferable' => in_array($license->getStatus(), ['active', 'inactive'], true),
|
'is_transferable' => in_array($license->getStatus(), ['active', 'inactive'], true),
|
||||||
|
'customer_secret' => ResponseSigner::getCustomerSecretForLicense($license->getLicenseKey()),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Track if package has at least one active license
|
// Track if package has at least one active license
|
||||||
|
|||||||
@@ -11,12 +11,43 @@ namespace Jeremias\WcLicensedProduct\License;
|
|||||||
|
|
||||||
use Jeremias\WcLicensedProduct\Installer;
|
use Jeremias\WcLicensedProduct\Installer;
|
||||||
use Jeremias\WcLicensedProduct\Product\LicensedProduct;
|
use Jeremias\WcLicensedProduct\Product\LicensedProduct;
|
||||||
|
use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
|
||||||
|
use Jeremias\WcLicensedProduct\Product\LicensedVariableProduct;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages license operations (CRUD, validation, generation)
|
* Manages license operations (CRUD, validation, generation)
|
||||||
*/
|
*/
|
||||||
class LicenseManager
|
class LicenseManager
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Check if a product is any type of licensed product
|
||||||
|
*
|
||||||
|
* @param \WC_Product $product Product to check
|
||||||
|
* @return bool True if product is licensed (simple or variable or variation)
|
||||||
|
*/
|
||||||
|
public function isLicensedProduct(\WC_Product $product): bool
|
||||||
|
{
|
||||||
|
// Simple licensed product
|
||||||
|
if ($product->is_type('licensed')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable licensed product
|
||||||
|
if ($product->is_type('licensed-variable')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variation of a licensed-variable product
|
||||||
|
if ($product->is_type('variation') && $product->get_parent_id()) {
|
||||||
|
$parent = wc_get_product($product->get_parent_id());
|
||||||
|
if ($parent && $parent->is_type('licensed-variable')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a unique license key
|
* Generate a unique license key
|
||||||
*/
|
*/
|
||||||
@@ -40,32 +71,63 @@ class LicenseManager
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a license for a completed order
|
* Generate a license for a completed order
|
||||||
|
*
|
||||||
|
* @param int $orderId Order ID
|
||||||
|
* @param int $productId Product ID (parent product for variations)
|
||||||
|
* @param int $customerId Customer ID
|
||||||
|
* @param string $domain Domain to bind the license to
|
||||||
|
* @param int|null $variationId Optional variation ID for variable licensed products
|
||||||
|
* @return License|null Generated license or null on failure
|
||||||
*/
|
*/
|
||||||
public function generateLicense(
|
public function generateLicense(
|
||||||
int $orderId,
|
int $orderId,
|
||||||
int $productId,
|
int $productId,
|
||||||
int $customerId,
|
int $customerId,
|
||||||
string $domain
|
string $domain,
|
||||||
|
?int $variationId = null
|
||||||
): ?License {
|
): ?License {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// Normalize domain first for duplicate detection
|
// Normalize domain first for duplicate detection
|
||||||
$normalizedDomain = $this->normalizeDomain($domain);
|
$normalizedDomain = $this->normalizeDomain($domain);
|
||||||
|
|
||||||
// Check if license already exists for this order, product, and domain
|
// Check if license already exists for this order, product, domain, and variation
|
||||||
$existing = $this->getLicenseByOrderProductAndDomain($orderId, $productId, $normalizedDomain);
|
$existing = $this->getLicenseByOrderProductDomainAndVariation($orderId, $productId, $normalizedDomain, $variationId);
|
||||||
if ($existing) {
|
if ($existing) {
|
||||||
return $existing;
|
return $existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
$product = wc_get_product($productId);
|
// Load the product that has the license settings
|
||||||
if (!$product || !$product->is_type('licensed')) {
|
// For variations, load the variation; otherwise load the parent product
|
||||||
|
if ($variationId) {
|
||||||
|
$settingsProduct = wc_get_product($variationId);
|
||||||
|
$parentProduct = wc_get_product($productId);
|
||||||
|
|
||||||
|
// Verify parent is licensed-variable
|
||||||
|
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the proper variation class
|
||||||
|
if ($settingsProduct && !$settingsProduct instanceof LicensedProductVariation) {
|
||||||
|
$settingsProduct = new LicensedProductVariation($variationId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$settingsProduct = wc_get_product($productId);
|
||||||
|
|
||||||
|
// Check if this is a licensed product (simple)
|
||||||
|
if (!$settingsProduct || !$settingsProduct->is_type('licensed')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have the LicensedProduct instance for type hints
|
// Ensure we have the LicensedProduct instance for type hints
|
||||||
if (!$product instanceof LicensedProduct) {
|
if (!$settingsProduct instanceof LicensedProduct) {
|
||||||
$product = new LicensedProduct($productId);
|
$settingsProduct = new LicensedProduct($productId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$settingsProduct) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique license key
|
// Generate unique license key
|
||||||
@@ -74,16 +136,16 @@ class LicenseManager
|
|||||||
$licenseKey = $this->generateLicenseKey();
|
$licenseKey = $this->generateLicenseKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate expiration date
|
// Calculate expiration date from the settings product (variation or parent)
|
||||||
$expiresAt = null;
|
$expiresAt = null;
|
||||||
$validityDays = $product->get_validity_days();
|
$validityDays = $settingsProduct->get_validity_days();
|
||||||
if ($validityDays !== null && $validityDays > 0) {
|
if ($validityDays !== null && $validityDays > 0) {
|
||||||
$expiresAt = (new \DateTimeImmutable())->modify("+{$validityDays} days")->format('Y-m-d H:i:s');
|
$expiresAt = (new \DateTimeImmutable())->modify("+{$validityDays} days")->format('Y-m-d H:i:s');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine version ID if bound to version
|
// Determine version ID if bound to version (always use parent product ID for versions)
|
||||||
$versionId = null;
|
$versionId = null;
|
||||||
if ($product->is_bound_to_version()) {
|
if ($settingsProduct->is_bound_to_version()) {
|
||||||
$versionId = $this->getCurrentVersionId($productId);
|
$versionId = $this->getCurrentVersionId($productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +161,7 @@ class LicenseManager
|
|||||||
'version_id' => $versionId,
|
'version_id' => $versionId,
|
||||||
'status' => License::STATUS_ACTIVE,
|
'status' => License::STATUS_ACTIVE,
|
||||||
'activations_count' => 1,
|
'activations_count' => 1,
|
||||||
'max_activations' => $product->get_max_activations(),
|
'max_activations' => $settingsProduct->get_max_activations(),
|
||||||
'expires_at' => $expiresAt,
|
'expires_at' => $expiresAt,
|
||||||
],
|
],
|
||||||
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
||||||
@@ -112,6 +174,16 @@ class LicenseManager
|
|||||||
return $this->getLicenseById((int) $wpdb->insert_id);
|
return $this->getLicenseById((int) $wpdb->insert_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get license by order, product, domain, and optional variation
|
||||||
|
*/
|
||||||
|
public function getLicenseByOrderProductDomainAndVariation(int $orderId, int $productId, string $domain, ?int $variationId = null): ?License
|
||||||
|
{
|
||||||
|
// For now, just use the existing method since we don't store variation_id in licenses table yet
|
||||||
|
// In the future, we could add a variation_id column to the licenses table
|
||||||
|
return $this->getLicenseByOrderProductAndDomain($orderId, $productId, $domain);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get license by ID
|
* Get license by ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -227,23 +227,35 @@ final class Plugin
|
|||||||
$orderId = $order->get_id();
|
$orderId = $order->get_id();
|
||||||
$customerId = $order->get_customer_id();
|
$customerId = $order->get_customer_id();
|
||||||
|
|
||||||
// Index domains by product ID for quick lookup
|
// Index domains by product ID (and variation ID for variable products)
|
||||||
$domainsByProduct = [];
|
$domainsByProduct = [];
|
||||||
foreach ($domainData as $item) {
|
foreach ($domainData as $item) {
|
||||||
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
|
if (isset($item['product_id']) && isset($item['domains']) && is_array($item['domains'])) {
|
||||||
$domainsByProduct[(int) $item['product_id']] = $item['domains'];
|
$productId = (int) $item['product_id'];
|
||||||
|
$variationId = isset($item['variation_id']) ? (int) $item['variation_id'] : 0;
|
||||||
|
$key = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||||
|
$domainsByProduct[$key] = [
|
||||||
|
'domains' => $item['domains'],
|
||||||
|
'variation_id' => $variationId,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate licenses for each licensed product
|
// Generate licenses for each licensed product
|
||||||
foreach ($order->get_items() as $item) {
|
foreach ($order->get_items() as $item) {
|
||||||
$product = $item->get_product();
|
$product = $item->get_product();
|
||||||
if (!$product || !$product->is_type('licensed')) {
|
if (!$product || !$this->licenseManager->isLicensedProduct($product)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$productId = $product->get_id();
|
// Get the parent product ID (for variations, this is the main product)
|
||||||
$domains = $domainsByProduct[$productId] ?? [];
|
$productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
|
||||||
|
$variationId = $item->get_variation_id();
|
||||||
|
|
||||||
|
// Look up domains - first try with variation, then without
|
||||||
|
$key = $variationId > 0 ? "{$productId}_{$variationId}" : (string) $productId;
|
||||||
|
$domainInfo = $domainsByProduct[$key] ?? $domainsByProduct[(string) $productId] ?? null;
|
||||||
|
$domains = $domainInfo['domains'] ?? [];
|
||||||
|
|
||||||
// Generate a license for each domain
|
// Generate a license for each domain
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
@@ -252,7 +264,8 @@ final class Plugin
|
|||||||
$orderId,
|
$orderId,
|
||||||
$productId,
|
$productId,
|
||||||
$customerId,
|
$customerId,
|
||||||
$domain
|
$domain,
|
||||||
|
$variationId > 0 ? $variationId : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,12 +284,17 @@ final class Plugin
|
|||||||
|
|
||||||
foreach ($order->get_items() as $item) {
|
foreach ($order->get_items() as $item) {
|
||||||
$product = $item->get_product();
|
$product = $item->get_product();
|
||||||
if ($product && $product->is_type('licensed')) {
|
if ($product && $this->licenseManager->isLicensedProduct($product)) {
|
||||||
|
// Get the parent product ID (for variations, this is the main product)
|
||||||
|
$productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
|
||||||
|
$variationId = $item->get_variation_id();
|
||||||
|
|
||||||
$this->licenseManager->generateLicense(
|
$this->licenseManager->generateLicense(
|
||||||
$order->get_id(),
|
$order->get_id(),
|
||||||
$product->get_id(),
|
$productId,
|
||||||
$order->get_customer_id(),
|
$order->get_customer_id(),
|
||||||
$domain
|
$domain,
|
||||||
|
$variationId > 0 ? $variationId : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ namespace Jeremias\WcLicensedProduct\Product;
|
|||||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers and handles the Licensed product type for WooCommerce
|
* Registers and handles the Licensed product types for WooCommerce
|
||||||
|
* Supports both simple licensed products and variable licensed products
|
||||||
*/
|
*/
|
||||||
final class LicensedProductType
|
final class LicensedProductType
|
||||||
{
|
{
|
||||||
@@ -29,7 +30,7 @@ final class LicensedProductType
|
|||||||
*/
|
*/
|
||||||
private function registerHooks(): void
|
private function registerHooks(): void
|
||||||
{
|
{
|
||||||
// Register product type
|
// Register product types
|
||||||
add_filter('product_type_selector', [$this, 'addProductType']);
|
add_filter('product_type_selector', [$this, 'addProductType']);
|
||||||
add_filter('woocommerce_product_class', [$this, 'getProductClass'], 10, 2);
|
add_filter('woocommerce_product_class', [$this, 'getProductClass'], 10, 2);
|
||||||
|
|
||||||
@@ -39,9 +40,11 @@ final class LicensedProductType
|
|||||||
|
|
||||||
// Save product meta
|
// Save product meta
|
||||||
add_action('woocommerce_process_product_meta_licensed', [$this, 'saveProductMeta']);
|
add_action('woocommerce_process_product_meta_licensed', [$this, 'saveProductMeta']);
|
||||||
|
add_action('woocommerce_process_product_meta_licensed-variable', [$this, 'saveProductMeta']);
|
||||||
|
|
||||||
// Show price and add to cart for licensed products
|
// Show price and add to cart for licensed products
|
||||||
add_action('woocommerce_licensed_add_to_cart', [$this, 'addToCartTemplate']);
|
add_action('woocommerce_licensed_add_to_cart', [$this, 'addToCartTemplate']);
|
||||||
|
add_action('woocommerce_licensed-variable_add_to_cart', [$this, 'variableAddToCartTemplate']);
|
||||||
|
|
||||||
// Make product virtual by default
|
// Make product virtual by default
|
||||||
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
|
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
|
||||||
@@ -51,25 +54,48 @@ final class LicensedProductType
|
|||||||
|
|
||||||
// Enqueue frontend CSS for licensed products on single product pages
|
// Enqueue frontend CSS for licensed products on single product pages
|
||||||
add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
|
add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
|
||||||
|
|
||||||
|
// Variable product support - variation settings
|
||||||
|
add_action('woocommerce_variation_options', [$this, 'addVariationOptions'], 10, 3);
|
||||||
|
add_action('woocommerce_product_after_variable_attributes', [$this, 'addVariationFields'], 10, 3);
|
||||||
|
add_action('woocommerce_save_product_variation', [$this, 'saveVariationFields'], 10, 2);
|
||||||
|
|
||||||
|
// Admin scripts for licensed-variable type
|
||||||
|
add_action('admin_footer', [$this, 'addVariableProductScripts']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add product type to selector
|
* Add product types to selector
|
||||||
*/
|
*/
|
||||||
public function addProductType(array $types): array
|
public function addProductType(array $types): array
|
||||||
{
|
{
|
||||||
$types['licensed'] = __('Licensed Product', 'wc-licensed-product');
|
$types['licensed'] = __('Licensed Product', 'wc-licensed-product');
|
||||||
|
$types['licensed-variable'] = __('Licensed Variable Product', 'wc-licensed-product');
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get product class for licensed type
|
* Get product class for licensed types
|
||||||
*/
|
*/
|
||||||
public function getProductClass(string $className, string $productType): string
|
public function getProductClass(string $className, string $productType): string
|
||||||
{
|
{
|
||||||
if ($productType === 'licensed') {
|
if ($productType === 'licensed') {
|
||||||
return LicensedProduct::class;
|
return LicensedProduct::class;
|
||||||
}
|
}
|
||||||
|
if ($productType === 'licensed-variable') {
|
||||||
|
return LicensedVariableProduct::class;
|
||||||
|
}
|
||||||
|
// Handle variations of licensed-variable products
|
||||||
|
if ($productType === 'variation') {
|
||||||
|
// Check if parent is licensed-variable
|
||||||
|
global $post;
|
||||||
|
if ($post && $post->post_parent) {
|
||||||
|
$parentType = \WC_Product_Factory::get_product_type($post->post_parent);
|
||||||
|
if ($parentType === 'licensed-variable') {
|
||||||
|
return LicensedProductVariation::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return $className;
|
return $className;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +107,7 @@ final class LicensedProductType
|
|||||||
$tabs['licensed_product'] = [
|
$tabs['licensed_product'] = [
|
||||||
'label' => __('License Settings', 'wc-licensed-product'),
|
'label' => __('License Settings', 'wc-licensed-product'),
|
||||||
'target' => 'licensed_product_data',
|
'target' => 'licensed_product_data',
|
||||||
'class' => ['show_if_licensed'],
|
'class' => ['show_if_licensed', 'show_if_licensed-variable'],
|
||||||
'priority' => 21,
|
'priority' => 21,
|
||||||
];
|
];
|
||||||
return $tabs;
|
return $tabs;
|
||||||
@@ -236,9 +262,16 @@ final class LicensedProductType
|
|||||||
*/
|
*/
|
||||||
public function isVirtual(bool $isVirtual, \WC_Product $product): bool
|
public function isVirtual(bool $isVirtual, \WC_Product $product): bool
|
||||||
{
|
{
|
||||||
if ($product->is_type('licensed')) {
|
if ($product->is_type('licensed') || $product->is_type('licensed-variable')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Also handle variations of licensed-variable products
|
||||||
|
if ($product->is_type('variation') && $product->get_parent_id()) {
|
||||||
|
$parent = wc_get_product($product->get_parent_id());
|
||||||
|
if ($parent && $parent->is_type('licensed-variable')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return $isVirtual;
|
return $isVirtual;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +286,7 @@ final class LicensedProductType
|
|||||||
|
|
||||||
global $product;
|
global $product;
|
||||||
|
|
||||||
if (!$product || !$product->is_type('licensed')) {
|
if (!$product || (!$product->is_type('licensed') && !$product->is_type('licensed-variable'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,11 +305,11 @@ final class LicensedProductType
|
|||||||
{
|
{
|
||||||
global $product;
|
global $product;
|
||||||
|
|
||||||
if (!$product || !$product->is_type('licensed')) {
|
if (!$product || (!$product->is_type('licensed') && !$product->is_type('licensed-variable'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var LicensedProduct $product */
|
/** @var LicensedProduct|LicensedVariableProduct $product */
|
||||||
$version = $product->get_current_version();
|
$version = $product->get_current_version();
|
||||||
|
|
||||||
if (empty($version)) {
|
if (empty($version)) {
|
||||||
@@ -289,4 +322,200 @@ final class LicensedProductType
|
|||||||
esc_html($version)
|
esc_html($version)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add to cart template for variable licensed products
|
||||||
|
*/
|
||||||
|
public function variableAddToCartTemplate(): void
|
||||||
|
{
|
||||||
|
wc_get_template('single-product/add-to-cart/variable.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add variation options (checkboxes next to variation header)
|
||||||
|
*/
|
||||||
|
public function addVariationOptions(int $loop, array $variationData, \WP_Post $variation): void
|
||||||
|
{
|
||||||
|
// Check if parent is licensed-variable
|
||||||
|
$parentId = $variation->post_parent;
|
||||||
|
$parentProduct = wc_get_product($parentId);
|
||||||
|
|
||||||
|
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isVirtual = get_post_meta($variation->ID, '_virtual', true);
|
||||||
|
?>
|
||||||
|
<label class="tips" data-tip="<?php esc_attr_e('Licensed products are always virtual', 'wc-licensed-product'); ?>">
|
||||||
|
<input type="checkbox" class="checkbox" disabled checked />
|
||||||
|
<?php esc_html_e('Virtual', 'wc-licensed-product'); ?>
|
||||||
|
</label>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add variation fields for license settings
|
||||||
|
*/
|
||||||
|
public function addVariationFields(int $loop, array $variationData, \WP_Post $variation): void
|
||||||
|
{
|
||||||
|
// Check if parent is licensed-variable
|
||||||
|
$parentId = $variation->post_parent;
|
||||||
|
$parentProduct = wc_get_product($parentId);
|
||||||
|
|
||||||
|
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get variation values
|
||||||
|
$validityDays = get_post_meta($variation->ID, '_licensed_validity_days', true);
|
||||||
|
$maxActivations = get_post_meta($variation->ID, '_licensed_max_activations', true);
|
||||||
|
|
||||||
|
// Get parent defaults for placeholder
|
||||||
|
$parentValidityDays = $parentProduct->get_validity_days();
|
||||||
|
$parentMaxActivations = $parentProduct->get_max_activations();
|
||||||
|
|
||||||
|
$parentValidityDisplay = $parentValidityDays !== null
|
||||||
|
? sprintf(__('%d days', 'wc-licensed-product'), $parentValidityDays)
|
||||||
|
: __('Lifetime', 'wc-licensed-product');
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="wclp-variation-license-settings">
|
||||||
|
<p class="form-row form-row-first">
|
||||||
|
<label><?php esc_html_e('License Duration (Days)', 'wc-licensed-product'); ?></label>
|
||||||
|
<input type="number"
|
||||||
|
name="wclp_validity_days[<?php echo esc_attr($loop); ?>]"
|
||||||
|
class="short"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
placeholder="<?php echo esc_attr($parentValidityDisplay); ?>"
|
||||||
|
value="<?php echo esc_attr($validityDays); ?>"
|
||||||
|
/>
|
||||||
|
<span class="description"><?php esc_html_e('Leave empty for parent default. 0 = Lifetime.', 'wc-licensed-product'); ?></span>
|
||||||
|
</p>
|
||||||
|
<p class="form-row form-row-last">
|
||||||
|
<label><?php esc_html_e('Max Activations', 'wc-licensed-product'); ?></label>
|
||||||
|
<input type="number"
|
||||||
|
name="wclp_max_activations[<?php echo esc_attr($loop); ?>]"
|
||||||
|
class="short"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
placeholder="<?php echo esc_attr($parentMaxActivations); ?>"
|
||||||
|
value="<?php echo esc_attr($maxActivations); ?>"
|
||||||
|
/>
|
||||||
|
<span class="description"><?php esc_html_e('Leave empty for parent default.', 'wc-licensed-product'); ?></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.wclp-variation-license-settings {
|
||||||
|
background: #f8f8f8;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.wclp-variation-license-settings .description {
|
||||||
|
display: block;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save variation fields
|
||||||
|
*/
|
||||||
|
public function saveVariationFields(int $variationId, int $loop): void
|
||||||
|
{
|
||||||
|
// Check if parent is licensed-variable
|
||||||
|
$variation = wc_get_product($variationId);
|
||||||
|
if (!$variation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentProduct = wc_get_product($variation->get_parent_id());
|
||||||
|
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save validity days
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified by WooCommerce
|
||||||
|
if (isset($_POST['wclp_validity_days'][$loop])) {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
|
$validityDays = sanitize_text_field($_POST['wclp_validity_days'][$loop]);
|
||||||
|
if ($validityDays !== '') {
|
||||||
|
update_post_meta($variationId, '_licensed_validity_days', absint($validityDays));
|
||||||
|
} else {
|
||||||
|
delete_post_meta($variationId, '_licensed_validity_days');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save max activations
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
|
if (isset($_POST['wclp_max_activations'][$loop])) {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
|
$maxActivations = sanitize_text_field($_POST['wclp_max_activations'][$loop]);
|
||||||
|
if ($maxActivations !== '') {
|
||||||
|
update_post_meta($variationId, '_licensed_max_activations', absint($maxActivations));
|
||||||
|
} else {
|
||||||
|
delete_post_meta($variationId, '_licensed_max_activations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set variation as virtual (licensed products are always virtual)
|
||||||
|
update_post_meta($variationId, '_virtual', 'yes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add JavaScript for licensed-variable product type in admin
|
||||||
|
*/
|
||||||
|
public function addVariableProductScripts(): void
|
||||||
|
{
|
||||||
|
global $post, $pagenow;
|
||||||
|
|
||||||
|
if ($pagenow !== 'post.php' && $pagenow !== 'post-new.php') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$post || get_post_type($post) !== 'product') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// Show/hide panels based on product type
|
||||||
|
function toggleLicensedVariableOptions() {
|
||||||
|
var productType = $('#product-type').val();
|
||||||
|
|
||||||
|
if (productType === 'licensed-variable') {
|
||||||
|
// Show variable product options
|
||||||
|
$('.show_if_variable').show();
|
||||||
|
$('.hide_if_variable').hide();
|
||||||
|
|
||||||
|
// Show licensed product options
|
||||||
|
$('.show_if_licensed-variable').show();
|
||||||
|
$('.show_if_licensed').show();
|
||||||
|
|
||||||
|
// Show general and variations tabs
|
||||||
|
$('.general_tab').show();
|
||||||
|
$('.variations_tab').show();
|
||||||
|
|
||||||
|
// Hide shipping tab (virtual products)
|
||||||
|
$('.shipping_tab').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
toggleLicensedVariableOptions();
|
||||||
|
|
||||||
|
// On product type change
|
||||||
|
$('#product-type').on('change', function() {
|
||||||
|
toggleLicensedVariableOptions();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
196
src/Product/LicensedProductVariation.php
Normal file
196
src/Product/LicensedProductVariation.php
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Licensed Product Variation Class
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Product
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Product;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
|
use WC_Product_Variation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed Product Variation type extending WooCommerce Product Variation
|
||||||
|
*
|
||||||
|
* Each variation can have its own license duration settings.
|
||||||
|
*/
|
||||||
|
class LicensedProductVariation extends WC_Product_Variation
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct($product = 0)
|
||||||
|
{
|
||||||
|
parent::__construct($product);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed products are always virtual
|
||||||
|
*/
|
||||||
|
public function is_virtual(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get max activations for this variation
|
||||||
|
* Falls back to parent product, then to default settings
|
||||||
|
*/
|
||||||
|
public function get_max_activations(): int
|
||||||
|
{
|
||||||
|
// Check variation-specific setting first
|
||||||
|
$value = $this->get_meta('_licensed_max_activations', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
return max(1, (int) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to parent product
|
||||||
|
$parent = wc_get_product($this->get_parent_id());
|
||||||
|
if ($parent && method_exists($parent, 'get_max_activations')) {
|
||||||
|
return $parent->get_max_activations();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsController::getDefaultMaxActivations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if variation has custom max activations set
|
||||||
|
*/
|
||||||
|
public function has_custom_max_activations(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_max_activations', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validity days for this variation
|
||||||
|
* This is the primary license setting that varies per variation
|
||||||
|
* Falls back to parent product, then to default settings
|
||||||
|
*/
|
||||||
|
public function get_validity_days(): ?int
|
||||||
|
{
|
||||||
|
// Check variation-specific setting first
|
||||||
|
$value = $this->get_meta('_licensed_validity_days', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
$days = (int) $value;
|
||||||
|
// 0 means lifetime
|
||||||
|
return $days > 0 ? $days : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to parent product
|
||||||
|
$parent = wc_get_product($this->get_parent_id());
|
||||||
|
if ($parent && method_exists($parent, 'get_validity_days')) {
|
||||||
|
return $parent->get_validity_days();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsController::getDefaultValidityDays();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if variation has custom validity days set
|
||||||
|
*/
|
||||||
|
public function has_custom_validity_days(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_validity_days', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if license should be bound to major version
|
||||||
|
* Falls back to parent product, then to default settings
|
||||||
|
*/
|
||||||
|
public function is_bound_to_version(): bool
|
||||||
|
{
|
||||||
|
// Check variation-specific setting first
|
||||||
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
return $value === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to parent product
|
||||||
|
$parent = wc_get_product($this->get_parent_id());
|
||||||
|
if ($parent && method_exists($parent, 'is_bound_to_version')) {
|
||||||
|
return $parent->is_bound_to_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsController::getDefaultBindToVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if variation has custom bind to version setting
|
||||||
|
*/
|
||||||
|
public function has_custom_bind_to_version(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the license duration label for display
|
||||||
|
*/
|
||||||
|
public function get_license_duration_label(): string
|
||||||
|
{
|
||||||
|
$days = $this->get_validity_days();
|
||||||
|
|
||||||
|
if ($days === null) {
|
||||||
|
return __('Lifetime', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($days === 30) {
|
||||||
|
return __('Monthly', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($days === 90) {
|
||||||
|
return __('Quarterly', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($days === 365) {
|
||||||
|
return __('Yearly', 'wc-licensed-product');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
/* translators: %d: number of days */
|
||||||
|
_n('%d day', '%d days', $days, 'wc-licensed-product'),
|
||||||
|
$days
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current software version from parent product
|
||||||
|
*/
|
||||||
|
public function get_current_version(): string
|
||||||
|
{
|
||||||
|
$parent = wc_get_product($this->get_parent_id());
|
||||||
|
if ($parent && method_exists($parent, 'get_current_version')) {
|
||||||
|
return $parent->get_current_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
$versionManager = new VersionManager();
|
||||||
|
$latestVersion = $versionManager->getLatestVersion($this->get_parent_id());
|
||||||
|
|
||||||
|
return $latestVersion ? $latestVersion->getVersion() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get major version number from parent product
|
||||||
|
*/
|
||||||
|
public function get_major_version(): int
|
||||||
|
{
|
||||||
|
$parent = wc_get_product($this->get_parent_id());
|
||||||
|
if ($parent && method_exists($parent, 'get_major_version')) {
|
||||||
|
return $parent->get_major_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
$versionManager = new VersionManager();
|
||||||
|
$latestVersion = $versionManager->getLatestVersion($this->get_parent_id());
|
||||||
|
|
||||||
|
if ($latestVersion) {
|
||||||
|
return $latestVersion->getMajorVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
src/Product/LicensedVariableProduct.php
Normal file
151
src/Product/LicensedVariableProduct.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Licensed Variable Product Class
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Product
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Product;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
|
use WC_Product_Variable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed Variable Product type extending WooCommerce Variable Product
|
||||||
|
*
|
||||||
|
* This allows selling license subscriptions with different durations
|
||||||
|
* (e.g., monthly, yearly, lifetime) as product variations.
|
||||||
|
*/
|
||||||
|
class LicensedVariableProduct extends WC_Product_Variable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Product type
|
||||||
|
*/
|
||||||
|
protected $product_type = 'licensed-variable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct($product = 0)
|
||||||
|
{
|
||||||
|
parent::__construct($product);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product type
|
||||||
|
*/
|
||||||
|
public function get_type(): string
|
||||||
|
{
|
||||||
|
return 'licensed-variable';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed products are always virtual
|
||||||
|
*/
|
||||||
|
public function is_virtual(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed products are purchasable
|
||||||
|
*/
|
||||||
|
public function is_purchasable(): bool
|
||||||
|
{
|
||||||
|
return $this->exists() && $this->get_price() !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get max activations for this product (parent default)
|
||||||
|
* Falls back to default settings if not set on product
|
||||||
|
*/
|
||||||
|
public function get_max_activations(): int
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_max_activations', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
return max(1, (int) $value);
|
||||||
|
}
|
||||||
|
return SettingsController::getDefaultMaxActivations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product has custom max activations set
|
||||||
|
*/
|
||||||
|
public function has_custom_max_activations(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_max_activations', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validity days (parent default - variations override this)
|
||||||
|
* Falls back to default settings if not set on product
|
||||||
|
*/
|
||||||
|
public function get_validity_days(): ?int
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_validity_days', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
return (int) $value > 0 ? (int) $value : null;
|
||||||
|
}
|
||||||
|
return SettingsController::getDefaultValidityDays();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product has custom validity days set
|
||||||
|
*/
|
||||||
|
public function has_custom_validity_days(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_validity_days', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if license should be bound to major version
|
||||||
|
* Falls back to default settings if not set on product
|
||||||
|
*/
|
||||||
|
public function is_bound_to_version(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
return $value === 'yes';
|
||||||
|
}
|
||||||
|
return SettingsController::getDefaultBindToVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product has custom bind to version setting
|
||||||
|
*/
|
||||||
|
public function has_custom_bind_to_version(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current software version (derived from latest product version)
|
||||||
|
*/
|
||||||
|
public function get_current_version(): string
|
||||||
|
{
|
||||||
|
$versionManager = new VersionManager();
|
||||||
|
$latestVersion = $versionManager->getLatestVersion($this->get_id());
|
||||||
|
|
||||||
|
return $latestVersion ? $latestVersion->getVersion() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get major version number from version string
|
||||||
|
*/
|
||||||
|
public function get_major_version(): int
|
||||||
|
{
|
||||||
|
$versionManager = new VersionManager();
|
||||||
|
$latestVersion = $versionManager->getLatestVersion($this->get_id());
|
||||||
|
|
||||||
|
if ($latestVersion) {
|
||||||
|
return $latestVersion->getMajorVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,26 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if signing_enabled and license.customer_secret %}
|
||||||
|
<div class="license-row-secret">
|
||||||
|
<button type="button" class="secret-toggle" aria-expanded="false">
|
||||||
|
<span class="dashicons dashicons-lock"></span>
|
||||||
|
{{ __('API Verification Secret') }}
|
||||||
|
<span class="dashicons dashicons-arrow-down-alt2 toggle-arrow"></span>
|
||||||
|
</button>
|
||||||
|
<div class="secret-content" style="display: none;">
|
||||||
|
<p class="secret-description">
|
||||||
|
{{ __('Use this secret to verify signed API responses. Keep it secure.') }}
|
||||||
|
</p>
|
||||||
|
<div class="secret-value-wrapper">
|
||||||
|
<code class="secret-value">{{ esc_html(license.customer_secret) }}</code>
|
||||||
|
<button type="button" class="copy-secret-btn" data-secret="{{ esc_attr(license.customer_secret) }}" title="{{ __('Copy to clipboard') }}">
|
||||||
|
<span class="dashicons dashicons-clipboard"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WooCommerce Licensed Product
|
* Plugin Name: WooCommerce Licensed Product
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
||||||
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||||
* Version: 0.5.1
|
* Version: 0.5.3
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('WC_LICENSED_PRODUCT_VERSION', '0.5.1');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.5.3');
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user