You've already forked wc-licensed-product
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b670bacf27 | |||
| f8f6434342 | |||
| dace416608 | |||
| 72017f4c62 | |||
| f9efe698ea | |||
| d2e3b41a00 | |||
| 4b6fafe500 | |||
| d29697ac62 | |||
| 142500cab0 | |||
| 20fb39d1a1 | |||
| 953aa6c8e8 | |||
| db4966caf2 | |||
| 9c4232f14f | |||
| 0638767ce3 | |||
| 9826c8181e | |||
| fa972ceaf0 |
79
CHANGELOG.md
79
CHANGELOG.md
@@ -7,6 +7,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.15] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed tab rendering bug in WooCommerce product edit page when switching to licensed or licensed-variable product types
|
||||||
|
- Simplified JavaScript to avoid conflicts with WooCommerce's native show/hide logic
|
||||||
|
- Removed conflicting CSS rule for `.hide_if_licensed` that was causing layout issues
|
||||||
|
- License Settings tab now uses CSS class toggle (`.wclp-active`) instead of jQuery `.show()/.hide()` for proper display
|
||||||
|
- Variations tab now properly shows for licensed-variable products via `woocommerce_product_data_tabs` filter
|
||||||
|
|
||||||
|
## [0.5.14] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL:** Fixed Product Versions meta box not appearing for licensed-variable products
|
||||||
|
- Product Versions meta box now always added to product pages, visibility controlled via CSS/JavaScript
|
||||||
|
- Added `Installer::registerProductTypes()` to create product type terms in the `product_type` taxonomy
|
||||||
|
- Product type terms are now ensured to exist on `woocommerce_init` hook for existing installations
|
||||||
|
- Fixed License Settings tab and Product Versions visibility toggling when changing product types
|
||||||
|
|
||||||
|
## [0.5.13] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL:** Fixed licenses not showing in admin order form for licensed-variable products
|
||||||
|
- `OrderLicenseController` now uses `LicenseManager::isLicensedProduct()` for consistent product type detection
|
||||||
|
- Fixed expected licenses calculation for variable product orders
|
||||||
|
- Fixed manual license generation from admin order page for variable products
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Removed debug logging from all source files (PHP and JavaScript)
|
||||||
|
- Cleaned up checkout blocks integration, Store API extension, and checkout controller
|
||||||
|
|
||||||
|
## [0.5.12] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL:** Fixed stock indicator ("1 in stock") appearing in cart for licensed variable product variations
|
||||||
|
- Override `get_children()` with direct SQL query to bypass WooCommerce's `is_type('variable')` check
|
||||||
|
- Override `get_variation_attributes()` to properly load taxonomy attribute terms
|
||||||
|
- Override `get_variation_prices()` to prevent fatal error with null `$this->prices_array`
|
||||||
|
- Override `get_available_variations()` with empty `availability_html` for variations
|
||||||
|
- Added `is_type()` override to return true for both 'licensed-variable' and 'variable' type checks
|
||||||
|
- Added multiple stock-related filters: `woocommerce_get_availability_text`, `woocommerce_product_get_stock_quantity`, `woocommerce_product_variation_get_stock_quantity`
|
||||||
|
- Improved `isLicensedProductOrVariation()` check using `WC_Product_Factory::get_product_type()` for reliable parent type detection
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `LicensedProductVariation` now includes `get_availability()`, `managing_stock()`, and `is_purchasable()` overrides
|
||||||
|
- Simplified `isVirtual()` to use shared `isLicensedProductOrVariation()` helper
|
||||||
|
|
||||||
|
## [0.5.11] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL:** Fixed "sold out" message on licensed variable products by correcting `is_purchasable()` method
|
||||||
|
- Variable products don't have a direct price - `is_purchasable()` now delegates to parent `WC_Product_Variable` class
|
||||||
|
- Fixed variation class detection by using product ID parameter instead of unreliable global `$post`
|
||||||
|
- Product class filter now properly accepts all 4 WooCommerce filter parameters for reliable variation detection
|
||||||
|
|
||||||
|
## [0.5.10] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed licensed variable products not showing variations even when attributes are defined
|
||||||
|
- Re-load product via `wc_get_product()` to ensure correct class instance is used
|
||||||
|
- Removed overly strict type check that was preventing variations from displaying
|
||||||
|
- Now mirrors WooCommerce's standard `woocommerce_variable_add_to_cart()` implementation
|
||||||
|
|
||||||
|
## [0.5.9] - 2026-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed frontend error on licensed variable products when no attributes are defined
|
||||||
|
- Added null checks for `get_variation_attributes()`, `get_available_variations()`, and `get_default_attributes()`
|
||||||
|
- Show informative message instead of error when product has no variations configured
|
||||||
|
- Changed product type check from `instanceof` to `is_type()` for better compatibility
|
||||||
|
|
||||||
## [0.5.8] - 2026-01-27
|
## [0.5.8] - 2026-01-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
161
CLAUDE.md
161
CLAUDE.md
@@ -1495,3 +1495,164 @@ Removed redundant "Default" prefix from setting labels on the Default Settings p
|
|||||||
- Created release package: `releases/wc-licensed-product-0.5.7.zip` (856 KB)
|
- Created release package: `releases/wc-licensed-product-0.5.7.zip` (856 KB)
|
||||||
- SHA256: `ceb4d57598f576f4f172153ff80df8c180ecd4dca873cf109327fc5ac718930f`
|
- SHA256: `ceb4d57598f576f4f172153ff80df8c180ecd4dca873cf109327fc5ac718930f`
|
||||||
- Tagged as `v0.5.7` and pushed to `main` branch
|
- Tagged as `v0.5.7` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.5.8-0.5.11 - Licensed Variable Product Fixes
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Series of bug fixes for licensed variable products that were showing frontend errors and not displaying properly.
|
||||||
|
|
||||||
|
**v0.5.8 - Initial Fix:**
|
||||||
|
|
||||||
|
- Fixed critical error on frontend product pages for licensed variable products
|
||||||
|
- Variable product add-to-cart template now passes required variables (`available_variations`, `attributes`, `selected_attributes`)
|
||||||
|
- Added JavaScript event listeners for WooCommerce AJAX events to maintain admin variants tab visibility
|
||||||
|
|
||||||
|
**v0.5.9 - Null Checks:**
|
||||||
|
|
||||||
|
- Added null checks for `get_variation_attributes()`, `get_available_variations()`, and `get_default_attributes()`
|
||||||
|
- Show informative message instead of error when product has no variations configured
|
||||||
|
- Changed product type check from `instanceof` to `is_type()` for better compatibility
|
||||||
|
|
||||||
|
**v0.5.10 - Product Loading:**
|
||||||
|
|
||||||
|
- Re-load product via `wc_get_product()` to ensure correct class instance is used
|
||||||
|
- Removed overly strict type check that was preventing variations from displaying
|
||||||
|
|
||||||
|
**v0.5.11 - Final Fix:**
|
||||||
|
|
||||||
|
- **CRITICAL:** Fixed "sold out" message on licensed variable products
|
||||||
|
- `LicensedVariableProduct::is_purchasable()` now delegates to parent `WC_Product_Variable` class (variable products don't have direct prices - only variations do)
|
||||||
|
- Fixed `getProductClass()` filter to accept all 4 WooCommerce parameters and use product_id for reliable variation parent detection
|
||||||
|
- Added fallback to global `$post` when product_id not available
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Product/LicensedProductType.php` - Fixed `variableAddToCartTemplate()` and `getProductClass()` methods
|
||||||
|
- `src/Product/LicensedVariableProduct.php` - Fixed `is_purchasable()` method
|
||||||
|
- `wc-licensed-product.php` - Version bumps
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- WooCommerce `woocommerce_product_class` filter has 4 parameters: `$className`, `$productType`, `$postType`, `$productId`
|
||||||
|
- Variable products delegate purchasability to their variations - checking `get_price()` on parent is incorrect
|
||||||
|
- Variation parent detection must use product ID, not global `$post` which may not be set on frontend
|
||||||
|
|
||||||
|
**Release v0.5.11:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.5.11.zip` (857 KB)
|
||||||
|
- SHA256: `32571178bfa8f0d0a03ed05b498d5f9b3c860104393a96732e86a03b6de298d2`
|
||||||
|
- Committed to `dev` branch
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.5.12 - Stock Display Fix
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed stock indicator appearing in cart for licensed variable product variations.
|
||||||
|
|
||||||
|
**Bug Fix:**
|
||||||
|
|
||||||
|
- Fixed "1 in stock" message appearing in cart for licensed variable product variations
|
||||||
|
- Added multiple WooCommerce filter overrides to suppress stock display
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Product/LicensedVariableProduct.php` - Override `get_children()`, `get_variation_attributes()`, `get_variation_prices()`, `get_available_variations()`, `is_type()`
|
||||||
|
- `src/Product/LicensedProductVariation.php` - Added `get_availability()`, `managing_stock()`, `is_purchasable()` overrides
|
||||||
|
- `src/Product/LicensedProductType.php` - Added stock-related filter hooks
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- `get_children()` uses direct SQL query to bypass WooCommerce's `is_type('variable')` check
|
||||||
|
- `is_type()` override returns true for both 'licensed-variable' and 'variable' type checks
|
||||||
|
- Stock filters: `woocommerce_get_availability_text`, `woocommerce_product_get_stock_quantity`, `woocommerce_product_variation_get_stock_quantity`
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.5.13 - Admin Order License Display Fix
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed licenses not showing in admin order form for licensed-variable products and removed debug logging.
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- **CRITICAL:** Fixed licenses not appearing in admin order form for orders containing licensed-variable products
|
||||||
|
- `OrderLicenseController` now uses `LicenseManager::isLicensedProduct()` for consistent product type detection across 4 locations
|
||||||
|
- Fixed expected licenses calculation for variable product orders
|
||||||
|
- Fixed manual license generation from admin order page for variable products
|
||||||
|
|
||||||
|
**Cleanup:**
|
||||||
|
|
||||||
|
- Removed all debug `error_log()` calls from PHP source files
|
||||||
|
- Removed all debug `console.log()` calls from JavaScript files
|
||||||
|
- Files cleaned: Plugin.php, CheckoutBlocksIntegration.php, StoreApiExtension.php, CheckoutController.php, checkout-blocks.js
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Admin/OrderLicenseController.php` - Use `isLicensedProduct()` in 4 locations
|
||||||
|
- `src/Plugin.php` - Remove debug logging
|
||||||
|
- `src/Checkout/CheckoutBlocksIntegration.php` - Remove debug logging
|
||||||
|
- `src/Checkout/StoreApiExtension.php` - Remove debug logging
|
||||||
|
- `src/Checkout/CheckoutController.php` - Remove debug logging
|
||||||
|
- `assets/js/checkout-blocks.js` - Remove debug logging
|
||||||
|
|
||||||
|
**Release v0.5.13:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.5.13.zip` (1.0 MB)
|
||||||
|
- SHA256: `814710ad899529d0015494e4b332eace7d8e55aeda381fdf61f99274c0bf910c`
|
||||||
|
- Committed to `dev` branch
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.5.14 - Product Versions Meta Box Fix
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed Product Versions meta box not appearing for licensed-variable products in admin.
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- Product Versions meta box now always added to product pages, visibility controlled via CSS/JavaScript
|
||||||
|
- Added `Installer::registerProductTypes()` to create product type terms in the `product_type` taxonomy
|
||||||
|
- Product type terms are now ensured to exist on `woocommerce_init` hook for existing installations
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Admin/VersionAdminController.php` - Simplified `addVersionsMetaBox()` to always add meta box
|
||||||
|
- `src/Installer.php` - Added `registerProductTypes()` method
|
||||||
|
- `src/Product/LicensedProductType.php` - Added `ensureProductTypeTermsExist()` hook
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- WooCommerce's `WC_Product_Factory::get_product_type()` requires product type terms to exist in the `product_type` taxonomy
|
||||||
|
- Meta box visibility is controlled via JavaScript based on selected product type
|
||||||
|
- Taxonomy terms are registered on `woocommerce_init` hook to ensure WooCommerce is fully loaded
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.5.15 - Tab Rendering Fix
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Fixed tab rendering bug in WooCommerce product edit page when switching to licensed or licensed-variable product types.
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- Fixed tab rendering issue where License Settings and Variations tabs appeared shifted/overlapping
|
||||||
|
- Simplified JavaScript to avoid conflicts with WooCommerce's native show/hide logic
|
||||||
|
- Removed conflicting CSS rule for `.hide_if_licensed` that was causing layout issues
|
||||||
|
- License Settings tab now uses CSS class toggle (`.wclp-active`) instead of jQuery `.show()/.hide()`
|
||||||
|
- Variations tab properly shows for licensed-variable products via `woocommerce_product_data_tabs` filter
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Product/LicensedProductType.php` - Simplified `toggleOurElements()` JavaScript function, added `show_if_licensed-variable` class to variations tab
|
||||||
|
- `assets/css/admin.css` - Removed `.hide_if_licensed` rule, updated tab visibility CSS to target `li.licensed_product_options`
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- jQuery's `.show()` sets `display: block` which can break `<li>` element layouts in tab lists
|
||||||
|
- Using CSS class toggle (`addClass/removeClass`) preserves proper display values
|
||||||
|
- WooCommerce product data tabs use class pattern `{tab_key}_options` (e.g., `licensed_product_options`)
|
||||||
|
- The `woocommerce_product_data_tabs` filter allows adding classes to existing tabs like variations
|
||||||
|
|
||||||
|
**Release v0.5.15:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.5.15.zip` (862 KB)
|
||||||
|
- SHA256: `47407de49bae4c649644af64e87b44b32fb30eeb2d50890ff8c4bbb741059278`
|
||||||
|
- Committed to `dev` branch
|
||||||
|
|||||||
@@ -50,16 +50,21 @@ code.file-hash {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* License Product Tab - Hidden by default, shown via JS based on product type */
|
/* License Settings Tab - Hidden by default, shown via JS based on product type */
|
||||||
#woocommerce-product-data .show_if_licensed,
|
/* WooCommerce creates tab with class: {tab_key}_options (licensed_product_options) */
|
||||||
#woocommerce-product-data .show_if_licensed-variable {
|
#woocommerce-product-data ul.wc-tabs li.licensed_product_options {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#woocommerce-product-data .hide_if_licensed {
|
/* When shown, restore proper display for tab list items */
|
||||||
display: none !important;
|
#woocommerce-product-data ul.wc-tabs li.licensed_product_options.wclp-active {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Variations tab visibility for licensed-variable is handled by WooCommerce */
|
||||||
|
/* We add show_if_licensed-variable class to the variations tab via PHP filter */
|
||||||
|
|
||||||
|
|
||||||
/* Action Buttons */
|
/* Action Buttons */
|
||||||
.wp-list-table .button-link-delete {
|
.wp-list-table .button-link-delete {
|
||||||
color: #a00;
|
color: #a00;
|
||||||
|
|||||||
@@ -18,12 +18,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { getSetting } = wc.wcSettings;
|
const { getSetting } = wc.wcSettings;
|
||||||
const { createElement, useState } = wp.element;
|
const { createElement, useState, useEffect, useCallback } = wp.element;
|
||||||
const { TextControl } = wp.components;
|
const { TextControl } = wp.components;
|
||||||
const { __ } = wp.i18n;
|
const { __ } = wp.i18n;
|
||||||
|
|
||||||
// Get available exports from blocksCheckout
|
// Get available exports from blocksCheckout
|
||||||
const { ExperimentalOrderMeta } = wc.blocksCheckout;
|
const { ExperimentalOrderMeta, extensionCartUpdate } = wc.blocksCheckout;
|
||||||
|
|
||||||
|
// Debounce function for API updates
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Get settings from PHP
|
// Get settings from PHP
|
||||||
const settings = getSetting('wc-licensed-product_data', {});
|
const settings = getSetting('wc-licensed-product_data', {});
|
||||||
@@ -59,6 +72,23 @@
|
|||||||
const [domain, setDomain] = useState('');
|
const [domain, setDomain] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
// Debounced API update function
|
||||||
|
const updateStoreApi = useCallback(
|
||||||
|
debounce((normalizedDomain) => {
|
||||||
|
if (extensionCartUpdate) {
|
||||||
|
extensionCartUpdate({
|
||||||
|
namespace: 'wc-licensed-product',
|
||||||
|
data: {
|
||||||
|
licensed_product_domain: normalizedDomain,
|
||||||
|
},
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[WCLP] Store API update error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = (value) => {
|
const handleChange = (value) => {
|
||||||
const normalized = normalizeDomain(value);
|
const normalized = normalizeDomain(value);
|
||||||
setDomain(normalized);
|
setDomain(normalized);
|
||||||
@@ -67,9 +97,11 @@
|
|||||||
setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'));
|
setError(settings.validationError || __('Please enter a valid domain.', 'wc-licensed-product'));
|
||||||
} else {
|
} else {
|
||||||
setError('');
|
setError('');
|
||||||
|
// Update Store API when valid
|
||||||
|
updateStoreApi(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store in hidden input for form submission
|
// Store in hidden input for form submission (fallback)
|
||||||
const hiddenInput = document.getElementById('wclp-domain-hidden');
|
const hiddenInput = document.getElementById('wclp-domain-hidden');
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
hiddenInput.value = normalized;
|
hiddenInput.value = normalized;
|
||||||
@@ -135,6 +167,23 @@
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
|
// Debounced API update function
|
||||||
|
const updateStoreApi = useCallback(
|
||||||
|
debounce((domainsData) => {
|
||||||
|
if (extensionCartUpdate) {
|
||||||
|
extensionCartUpdate({
|
||||||
|
namespace: 'wc-licensed-product',
|
||||||
|
data: {
|
||||||
|
licensed_product_domains: domainsData,
|
||||||
|
},
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[WCLP] Store API update error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 500),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
if (!products.length) {
|
if (!products.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -174,7 +223,7 @@
|
|||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
// Update hidden field with variation support
|
// Build domain data for Store API
|
||||||
const data = products.map(p => {
|
const data = products.map(p => {
|
||||||
const pKey = getProductKey(p);
|
const pKey = getProductKey(p);
|
||||||
const doms = newDomains[pKey] || [];
|
const doms = newDomains[pKey] || [];
|
||||||
@@ -188,6 +237,10 @@
|
|||||||
return entry;
|
return entry;
|
||||||
}).filter(item => item.domains.length > 0);
|
}).filter(item => item.domains.length > 0);
|
||||||
|
|
||||||
|
// Update Store API
|
||||||
|
updateStoreApi(data);
|
||||||
|
|
||||||
|
// Update hidden field (fallback)
|
||||||
const hiddenInput = document.getElementById('wclp-domains-hidden');
|
const hiddenInput = document.getElementById('wclp-domains-hidden');
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
hiddenInput.value = JSON.stringify(data);
|
hiddenInput.value = JSON.stringify(data);
|
||||||
@@ -273,11 +326,13 @@
|
|||||||
|
|
||||||
if (registerPlugin) {
|
if (registerPlugin) {
|
||||||
registerPlugin('wc-licensed-product-domain-fields', {
|
registerPlugin('wc-licensed-product-domain-fields', {
|
||||||
render: () => createElement(
|
render: () => {
|
||||||
ExperimentalOrderMeta,
|
return createElement(
|
||||||
{},
|
ExperimentalOrderMeta,
|
||||||
createElement(LicenseDomainsBlock)
|
{},
|
||||||
),
|
createElement(LicenseDomainsBlock)
|
||||||
|
);
|
||||||
|
},
|
||||||
scope: 'woocommerce-checkout',
|
scope: 'woocommerce-checkout',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -379,6 +434,68 @@
|
|||||||
} else {
|
} else {
|
||||||
insertionPoint.appendChild(container);
|
insertionPoint.appendChild(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add event listeners to sync with Store API
|
||||||
|
const debouncedUpdate = debounce(function() {
|
||||||
|
if (!extensionCartUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.isMultiDomainEnabled && settings.licensedProducts) {
|
||||||
|
// Collect multi-domain data
|
||||||
|
const domainsData = settings.licensedProducts.map(function(product) {
|
||||||
|
const productKey = product.variation_id && product.variation_id > 0
|
||||||
|
? product.product_id + '_' + product.variation_id
|
||||||
|
: String(product.product_id);
|
||||||
|
|
||||||
|
const domains = [];
|
||||||
|
for (let i = 0; i < product.quantity; i++) {
|
||||||
|
const input = container.querySelector('input[name="licensed_domains[' + productKey + '][' + i + ']"]');
|
||||||
|
if (input && input.value.trim()) {
|
||||||
|
domains.push(normalizeDomain(input.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
product_id: product.product_id,
|
||||||
|
domains: domains,
|
||||||
|
};
|
||||||
|
if (product.variation_id && product.variation_id > 0) {
|
||||||
|
entry.variation_id = product.variation_id;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}).filter(function(item) { return item.domains.length > 0; });
|
||||||
|
|
||||||
|
extensionCartUpdate({
|
||||||
|
namespace: 'wc-licensed-product',
|
||||||
|
data: {
|
||||||
|
licensed_product_domains: domainsData,
|
||||||
|
},
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('[WCLP] Store API update error:', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Single domain
|
||||||
|
const input = container.querySelector('input[name="licensed_product_domain"]');
|
||||||
|
if (input) {
|
||||||
|
const domain = normalizeDomain(input.value);
|
||||||
|
extensionCartUpdate({
|
||||||
|
namespace: 'wc-licensed-product',
|
||||||
|
data: {
|
||||||
|
licensed_product_domain: domain,
|
||||||
|
},
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('[WCLP] Store API update error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Attach event listeners to all domain inputs
|
||||||
|
container.querySelectorAll('input[type="text"]').forEach(function(input) {
|
||||||
|
input.addEventListener('input', debouncedUpdate);
|
||||||
|
input.addEventListener('change', debouncedUpdate);
|
||||||
|
});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
Binary file not shown.
@@ -3,10 +3,10 @@
|
|||||||
# This file is distributed under the GPL-2.0-or-later.
|
# This file is distributed under the GPL-2.0-or-later.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WC Licensed Product 0.5.0\n"
|
"Project-Id-Version: WC Licensed Product 0.6.0\n"
|
||||||
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
||||||
"POT-Creation-Date: 2026-01-27 13:34+0100\n"
|
"POT-Creation-Date: 2026-01-27 18:00+0100\n"
|
||||||
"PO-Revision-Date: 2026-01-25T18:30:00+00:00\n"
|
"PO-Revision-Date: 2026-01-27T18:00: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"
|
||||||
"Language: de_CH\n"
|
"Language: de_CH\n"
|
||||||
@@ -311,10 +311,10 @@ msgstr "Speichern"
|
|||||||
#: 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/Admin/SettingsController.php:192
|
||||||
#: src/Product/LicensedProductVariation.php:139
|
#: src/Product/LicensedProductVariation.php:194
|
||||||
#: src/Product/LicensedProductType.php:136
|
#: src/Product/LicensedProductType.php:164
|
||||||
#: src/Product/LicensedProductType.php:184
|
#: src/Product/LicensedProductType.php:212
|
||||||
#: src/Product/LicensedProductType.php:403
|
#: src/Product/LicensedProductType.php:553
|
||||||
#: src/Frontend/AccountController.php:286
|
#: src/Frontend/AccountController.php:286
|
||||||
msgid "Lifetime"
|
msgid "Lifetime"
|
||||||
msgstr "Lebenslang"
|
msgstr "Lebenslang"
|
||||||
@@ -492,6 +492,7 @@ msgstr "Lizenz erfolgreich verlängert."
|
|||||||
msgid "License set to lifetime successfully."
|
msgid "License set to lifetime successfully."
|
||||||
msgstr "Lizenz erfolgreich auf lebenslang gesetzt."
|
msgstr "Lizenz erfolgreich auf lebenslang gesetzt."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1106
|
#: src/Admin/AdminController.php:1106
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license activated."
|
msgid "%d license activated."
|
||||||
@@ -499,6 +500,7 @@ msgid_plural "%d licenses activated."
|
|||||||
msgstr[0] "%d Lizenz aktiviert."
|
msgstr[0] "%d Lizenz aktiviert."
|
||||||
msgstr[1] "%d Lizenzen aktiviert."
|
msgstr[1] "%d Lizenzen aktiviert."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1114
|
#: src/Admin/AdminController.php:1114
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license deactivated."
|
msgid "%d license deactivated."
|
||||||
@@ -506,6 +508,7 @@ msgid_plural "%d licenses deactivated."
|
|||||||
msgstr[0] "%d Lizenz deaktiviert."
|
msgstr[0] "%d Lizenz deaktiviert."
|
||||||
msgstr[1] "%d Lizenzen deaktiviert."
|
msgstr[1] "%d Lizenzen deaktiviert."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1122
|
#: src/Admin/AdminController.php:1122
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license revoked."
|
msgid "%d license revoked."
|
||||||
@@ -513,6 +516,7 @@ msgid_plural "%d licenses revoked."
|
|||||||
msgstr[0] "%d Lizenz widerrufen."
|
msgstr[0] "%d Lizenz widerrufen."
|
||||||
msgstr[1] "%d Lizenzen widerrufen."
|
msgstr[1] "%d Lizenzen widerrufen."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1130
|
#: src/Admin/AdminController.php:1130
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license deleted."
|
msgid "%d license deleted."
|
||||||
@@ -520,6 +524,7 @@ msgid_plural "%d licenses deleted."
|
|||||||
msgstr[0] "%d Lizenz gelöscht."
|
msgstr[0] "%d Lizenz gelöscht."
|
||||||
msgstr[1] "%d Lizenzen gelöscht."
|
msgstr[1] "%d Lizenzen gelöscht."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1138
|
#: src/Admin/AdminController.php:1138
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license extended."
|
msgid "%d license extended."
|
||||||
@@ -541,6 +546,7 @@ msgstr ""
|
|||||||
msgid "No licenses to export."
|
msgid "No licenses to export."
|
||||||
msgstr "Keine Lizenzen zum Exportieren."
|
msgstr "Keine Lizenzen zum Exportieren."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses imported
|
||||||
#: src/Admin/AdminController.php:1159
|
#: src/Admin/AdminController.php:1159
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license imported."
|
msgid "%d license imported."
|
||||||
@@ -548,6 +554,7 @@ msgid_plural "%d licenses imported."
|
|||||||
msgstr[0] "%d Lizenz importiert."
|
msgstr[0] "%d Lizenz importiert."
|
||||||
msgstr[1] "%d Lizenzen importiert."
|
msgstr[1] "%d Lizenzen importiert."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses updated
|
||||||
#: src/Admin/AdminController.php:1166
|
#: src/Admin/AdminController.php:1166
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d updated."
|
msgid "%d updated."
|
||||||
@@ -555,6 +562,7 @@ msgid_plural "%d updated."
|
|||||||
msgstr[0] "%d aktualisiert."
|
msgstr[0] "%d aktualisiert."
|
||||||
msgstr[1] "%d aktualisiert."
|
msgstr[1] "%d aktualisiert."
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses skipped
|
||||||
#: src/Admin/AdminController.php:1174
|
#: src/Admin/AdminController.php:1174
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d skipped."
|
msgid "%d skipped."
|
||||||
@@ -562,6 +570,7 @@ msgid_plural "%d skipped."
|
|||||||
msgstr[0] "%d übersprungen."
|
msgstr[0] "%d übersprungen."
|
||||||
msgstr[1] "%d übersprungen."
|
msgstr[1] "%d übersprungen."
|
||||||
|
|
||||||
|
#. translators: %d: number of errors
|
||||||
#: src/Admin/AdminController.php:1182
|
#: src/Admin/AdminController.php:1182
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d error."
|
msgid "%d error."
|
||||||
@@ -1020,6 +1029,7 @@ msgstr "Domain bearbeiten"
|
|||||||
msgid "View in Licenses"
|
msgid "View in Licenses"
|
||||||
msgstr "In Lizenzen anzeigen"
|
msgstr "In Lizenzen anzeigen"
|
||||||
|
|
||||||
|
#. translators: %s: Link to licenses page
|
||||||
#: src/Admin/OrderLicenseController.php:280
|
#: src/Admin/OrderLicenseController.php:280
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "For more actions (revoke, extend, delete), go to the %s page."
|
msgid "For more actions (revoke, extend, delete), go to the %s page."
|
||||||
@@ -1166,8 +1176,8 @@ msgstr ""
|
|||||||
"Diese Einstellungen dienen als Standard für neue lizensierte Produkte. "
|
"Diese Einstellungen dienen als Standard für neue lizensierte Produkte. "
|
||||||
"Individuelle Produkteinstellungen überschreiben diese Standards."
|
"Individuelle Produkteinstellungen überschreiben diese Standards."
|
||||||
|
|
||||||
#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:154
|
#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:182
|
||||||
#: src/Product/LicensedProductType.php:420
|
#: src/Product/LicensedProductType.php:570
|
||||||
msgid "Max Activations"
|
msgid "Max Activations"
|
||||||
msgstr "Max. Aktivierungen"
|
msgstr "Max. Aktivierungen"
|
||||||
|
|
||||||
@@ -1175,7 +1185,7 @@ msgstr "Max. Aktivierungen"
|
|||||||
msgid "Default maximum number of domain activations per license."
|
msgid "Default maximum number of domain activations per license."
|
||||||
msgstr "Standard maximale Anzahl der Domain-Aktivierungen pro Lizenz."
|
msgstr "Standard maximale Anzahl der Domain-Aktivierungen pro Lizenz."
|
||||||
|
|
||||||
#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:172
|
#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:200
|
||||||
msgid "License Validity (Days)"
|
msgid "License Validity (Days)"
|
||||||
msgstr "Lizenz-Gültigkeit (Tage)"
|
msgstr "Lizenz-Gültigkeit (Tage)"
|
||||||
|
|
||||||
@@ -1187,7 +1197,7 @@ msgstr ""
|
|||||||
"Standard Anzahl Tage, die eine Lizenz gültig ist. Leer lassen oder auf 0 "
|
"Standard Anzahl Tage, die eine Lizenz gültig ist. Leer lassen oder auf 0 "
|
||||||
"setzen für lebenslange Lizenzen."
|
"setzen für lebenslange Lizenzen."
|
||||||
|
|
||||||
#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:190
|
#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:218
|
||||||
msgid "Bind to Major Version"
|
msgid "Bind to Major Version"
|
||||||
msgstr "An Hauptversion binden"
|
msgstr "An Hauptversion binden"
|
||||||
|
|
||||||
@@ -1215,6 +1225,7 @@ msgstr ""
|
|||||||
msgid "Expiration Warning Schedule"
|
msgid "Expiration Warning Schedule"
|
||||||
msgstr "Ablaufwarnung Zeitplan"
|
msgstr "Ablaufwarnung Zeitplan"
|
||||||
|
|
||||||
|
#. translators: %s: URL to WooCommerce email settings
|
||||||
#: src/Admin/SettingsController.php:230
|
#: src/Admin/SettingsController.php:230
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1346,6 +1357,7 @@ msgstr "Lizenz-Domains"
|
|||||||
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."
|
||||||
|
|
||||||
|
#. translators: %d: license number
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
||||||
#: src/Checkout/CheckoutController.php:224
|
#: src/Checkout/CheckoutController.php:224
|
||||||
#, php-format
|
#, php-format
|
||||||
@@ -1357,6 +1369,16 @@ msgstr "Lizenz %d:"
|
|||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr "erforderlich"
|
msgstr "erforderlich"
|
||||||
|
|
||||||
|
#: src/Checkout/CheckoutController.php:215
|
||||||
|
#, php-format
|
||||||
|
msgid "licensed_domains[%s][%d]"
|
||||||
|
msgstr "licensed_domains[%s][%d]"
|
||||||
|
|
||||||
|
#: src/Checkout/CheckoutController.php:216
|
||||||
|
#, php-format
|
||||||
|
msgid "licensed_domain_%s_%d"
|
||||||
|
msgstr "licensed_domain_%s_%d"
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:323
|
#: 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."
|
||||||
@@ -1365,16 +1387,19 @@ msgstr "Bitte geben Sie eine Domain für Ihre Lizenz ein."
|
|||||||
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."
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: license number
|
||||||
#: src/Checkout/CheckoutController.php:356
|
#: 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."
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: license number
|
||||||
#: src/Checkout/CheckoutController.php:371
|
#: 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."
|
||||||
|
|
||||||
|
#. translators: 1: domain name, 2: product name
|
||||||
#: src/Checkout/CheckoutController.php:385
|
#: src/Checkout/CheckoutController.php:385
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1438,68 +1463,74 @@ msgstr "Diese Lizenz ist für diese Domain nicht gültig."
|
|||||||
msgid "Attachment file not found."
|
msgid "Attachment file not found."
|
||||||
msgstr "Anhangs-Datei nicht gefunden."
|
msgstr "Anhangs-Datei nicht gefunden."
|
||||||
|
|
||||||
|
#. translators: 1: provided hash, 2: calculated hash
|
||||||
#: src/Product/VersionManager.php:177
|
#: src/Product/VersionManager.php:177
|
||||||
#, php-format
|
#, php-format
|
||||||
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
|
#: src/Product/LicensedProductVariation.php:198
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr "Monatlich"
|
msgstr "Monatlich"
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:147
|
#: src/Product/LicensedProductVariation.php:202
|
||||||
msgid "Quarterly"
|
msgid "Quarterly"
|
||||||
msgstr "Vierteljährlich"
|
msgstr "Vierteljährlich"
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:151
|
#: src/Product/LicensedProductVariation.php:206
|
||||||
msgid "Yearly"
|
msgid "Yearly"
|
||||||
msgstr "Jährlich"
|
msgstr "Jährlich"
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:156
|
#. translators: %d: number of days
|
||||||
|
#: src/Product/LicensedProductVariation.php:211
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d day"
|
msgid "%d day"
|
||||||
msgid_plural "%d days"
|
msgid_plural "%d days"
|
||||||
msgstr[0] "%d Tag"
|
msgstr[0] "%d Tag"
|
||||||
msgstr[1] "%d Tage"
|
msgstr[1] "%d Tage"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:72
|
#: src/Product/LicensedProductType.php:82
|
||||||
msgid "Licensed Product"
|
msgid "Licensed Product"
|
||||||
msgstr "Lizensiertes Produkt"
|
msgstr "Lizensiertes Produkt"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:73
|
#: src/Product/LicensedProductType.php:83
|
||||||
msgid "Licensed Variable Product"
|
msgid "Licensed Variable Product"
|
||||||
msgstr "Lizensiertes variables Produkt"
|
msgstr "Lizensiertes variables Produkt"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:108
|
#: src/Product/LicensedProductType.php:136
|
||||||
msgid "License Settings"
|
msgid "License Settings"
|
||||||
msgstr "Lizenz-Einstellungen"
|
msgstr "Lizenz-Einstellungen"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:135
|
#: src/Product/LicensedProductType.php:163
|
||||||
#: src/Product/LicensedProductType.php:402
|
#: src/Product/LicensedProductType.php:552
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d days"
|
msgid "%d days"
|
||||||
msgstr "%d Tage"
|
msgstr "%d Tage"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:145
|
#. translators: %s: URL to settings page
|
||||||
|
#: src/Product/LicensedProductType.php:173
|
||||||
#, 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:147
|
#: src/Product/LicensedProductType.php:175
|
||||||
msgid "WooCommerce > Settings > Licensed Products"
|
msgid "WooCommerce > Settings > Licensed Products"
|
||||||
msgstr "WooCommerce > Einstellungen > Lizensierte Produkte"
|
msgstr "WooCommerce > Einstellungen > Lizensierte Produkte"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:157
|
#. translators: %d: default max activations value
|
||||||
|
#: src/Product/LicensedProductType.php:185
|
||||||
#, 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:175
|
#. translators: %s: default validity value
|
||||||
|
#: src/Product/LicensedProductType.php:203
|
||||||
#, 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:193
|
#. translators: %s: default bind to version value (Yes/No)
|
||||||
|
#: src/Product/LicensedProductType.php:221
|
||||||
#, 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. "
|
||||||
@@ -1508,35 +1539,35 @@ 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:194
|
#: src/Product/LicensedProductType.php:222
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:194
|
#: src/Product/LicensedProductType.php:222
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr "Nein"
|
msgstr "Nein"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:329
|
#: src/Product/LicensedProductType.php:447
|
||||||
msgid "Version:"
|
msgid "Version:"
|
||||||
msgstr "Version:"
|
msgstr "Version:"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:373
|
#: src/Product/LicensedProductType.php:523
|
||||||
msgid "Licensed products are always virtual"
|
msgid "Licensed products are always virtual"
|
||||||
msgstr "Lizenzierte Produkte sind immer virtuell"
|
msgstr "Lizenzierte Produkte sind immer virtuell"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:375
|
#: src/Product/LicensedProductType.php:525
|
||||||
msgid "Virtual"
|
msgid "Virtual"
|
||||||
msgstr "Virtuell"
|
msgstr "Virtuell"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:408
|
#: src/Product/LicensedProductType.php:558
|
||||||
msgid "License Duration (Days)"
|
msgid "License Duration (Days)"
|
||||||
msgstr "Lizenz-Gültigkeit (Tage)"
|
msgstr "Lizenz-Gültigkeit (Tage)"
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:417
|
#: src/Product/LicensedProductType.php:567
|
||||||
msgid "Leave empty for parent default. 0 = Lifetime."
|
msgid "Leave empty for parent default. 0 = Lifetime."
|
||||||
msgstr "Leer lassen für übergeordneten Standard. 0 = Lebenslang."
|
msgstr "Leer lassen für übergeordneten Standard. 0 = Lebenslang."
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:429
|
#: src/Product/LicensedProductType.php:579
|
||||||
msgid "Leave empty for parent default."
|
msgid "Leave empty for parent default."
|
||||||
msgstr "Leer lassen für übergeordneten Standard."
|
msgstr "Leer lassen für übergeordneten Standard."
|
||||||
|
|
||||||
@@ -1599,6 +1630,7 @@ msgstr "Bitte melden Sie sich an, um Ihre Lizenzen zu sehen."
|
|||||||
msgid "You have no licenses yet."
|
msgid "You have no licenses yet."
|
||||||
msgstr "Sie haben noch keine Lizenzen."
|
msgstr "Sie haben noch keine Lizenzen."
|
||||||
|
|
||||||
|
#. translators: %s: order number
|
||||||
#: src/Frontend/AccountController.php:245
|
#: src/Frontend/AccountController.php:245
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Order #%s"
|
msgid "Order #%s"
|
||||||
@@ -1756,6 +1788,7 @@ msgstr ""
|
|||||||
"Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz vor "
|
"Um dieses Produkt weiterhin zu nutzen, verlängern Sie bitte Ihre Lizenz vor "
|
||||||
"dem Ablaufdatum."
|
"dem Ablaufdatum."
|
||||||
|
|
||||||
|
#. translators: %s: list of placeholders
|
||||||
#: src/Email/LicenseExpirationEmail.php:301
|
#: src/Email/LicenseExpirationEmail.php:301
|
||||||
#: src/Email/LicenseExpiredEmail.php:288
|
#: src/Email/LicenseExpiredEmail.php:288
|
||||||
#, php-format
|
#, php-format
|
||||||
@@ -1894,6 +1927,7 @@ msgstr ""
|
|||||||
msgid "Configure License"
|
msgid "Configure License"
|
||||||
msgstr "Lizenz konfigurieren"
|
msgstr "Lizenz konfigurieren"
|
||||||
|
|
||||||
|
#. translators: %s: WooCommerce plugin name
|
||||||
#: wc-licensed-product.php:61
|
#: wc-licensed-product.php:61
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%s requires WooCommerce to be installed and active."
|
msgid "%s requires WooCommerce to be installed and active."
|
||||||
@@ -1930,3 +1964,39 @@ msgstr ""
|
|||||||
|
|
||||||
#~ msgid "Licensed Domain:"
|
#~ msgid "Licensed Domain:"
|
||||||
#~ msgstr "Lizensierte Domain:"
|
#~ msgstr "Lizensierte Domain:"
|
||||||
|
|
||||||
|
#: src/Api/UpdateController.php:195
|
||||||
|
msgid "Licensed product not found."
|
||||||
|
msgstr "Lizenziertes Produkt nicht gefunden."
|
||||||
|
|
||||||
|
#: src/Api/UpdateController.php:207
|
||||||
|
msgid "No versions available for this product."
|
||||||
|
msgstr "Keine Versionen für dieses Produkt verfügbar."
|
||||||
|
|
||||||
|
#: src/Update/PluginUpdateChecker.php:295
|
||||||
|
msgid "WooCommerce plugin for selling licensed software products with domain-bound license keys."
|
||||||
|
msgstr "WooCommerce-Plugin zum Verkauf von lizenzierten Softwareprodukten mit domaingebundenen Lizenzschlüsseln."
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:163
|
||||||
|
msgid "Auto-Updates"
|
||||||
|
msgstr "Auto-Updates"
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:165
|
||||||
|
msgid "Configure automatic plugin updates from the license server."
|
||||||
|
msgstr "Automatische Plugin-Updates vom Lizenzserver konfigurieren."
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:169
|
||||||
|
msgid "Enable Auto-Updates"
|
||||||
|
msgstr "Auto-Updates aktivieren"
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:172
|
||||||
|
msgid "Automatically check for and receive plugin updates from the license server."
|
||||||
|
msgstr "Automatisch auf Plugin-Updates vom Lizenzserver prüfen und diese erhalten."
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:177
|
||||||
|
msgid "Check Frequency (Hours)"
|
||||||
|
msgstr "Prüfhäufigkeit (Stunden)"
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:180
|
||||||
|
msgid "How often to check for updates (in hours)."
|
||||||
|
msgstr "Wie oft auf Updates geprüft werden soll (in Stunden)."
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WC Licensed Product 0.5.8\n"
|
"Project-Id-Version: WC Licensed Product 0.6.0\n"
|
||||||
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
"Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
|
||||||
"POT-Creation-Date: 2026-01-27 13:34+0100\n"
|
"POT-Creation-Date: 2026-01-27 18:00+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"
|
||||||
@@ -304,10 +304,10 @@ msgstr ""
|
|||||||
#: 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/Admin/SettingsController.php:192
|
||||||
#: src/Product/LicensedProductVariation.php:139
|
#: src/Product/LicensedProductVariation.php:194
|
||||||
#: src/Product/LicensedProductType.php:136
|
#: src/Product/LicensedProductType.php:164
|
||||||
#: src/Product/LicensedProductType.php:184
|
#: src/Product/LicensedProductType.php:212
|
||||||
#: src/Product/LicensedProductType.php:403
|
#: src/Product/LicensedProductType.php:553
|
||||||
#: src/Frontend/AccountController.php:286
|
#: src/Frontend/AccountController.php:286
|
||||||
msgid "Lifetime"
|
msgid "Lifetime"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -485,6 +485,7 @@ msgstr ""
|
|||||||
msgid "License set to lifetime successfully."
|
msgid "License set to lifetime successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1106
|
#: src/Admin/AdminController.php:1106
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license activated."
|
msgid "%d license activated."
|
||||||
@@ -492,6 +493,7 @@ msgid_plural "%d licenses activated."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1114
|
#: src/Admin/AdminController.php:1114
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license deactivated."
|
msgid "%d license deactivated."
|
||||||
@@ -499,6 +501,7 @@ msgid_plural "%d licenses deactivated."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1122
|
#: src/Admin/AdminController.php:1122
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license revoked."
|
msgid "%d license revoked."
|
||||||
@@ -506,6 +509,7 @@ msgid_plural "%d licenses revoked."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1130
|
#: src/Admin/AdminController.php:1130
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license deleted."
|
msgid "%d license deleted."
|
||||||
@@ -513,6 +517,7 @@ msgid_plural "%d licenses deleted."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses
|
||||||
#: src/Admin/AdminController.php:1138
|
#: src/Admin/AdminController.php:1138
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license extended."
|
msgid "%d license extended."
|
||||||
@@ -532,6 +537,7 @@ msgstr ""
|
|||||||
msgid "No licenses to export."
|
msgid "No licenses to export."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses imported
|
||||||
#: src/Admin/AdminController.php:1159
|
#: src/Admin/AdminController.php:1159
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d license imported."
|
msgid "%d license imported."
|
||||||
@@ -539,6 +545,7 @@ msgid_plural "%d licenses imported."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses updated
|
||||||
#: src/Admin/AdminController.php:1166
|
#: src/Admin/AdminController.php:1166
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d updated."
|
msgid "%d updated."
|
||||||
@@ -546,6 +553,7 @@ msgid_plural "%d updated."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of licenses skipped
|
||||||
#: src/Admin/AdminController.php:1174
|
#: src/Admin/AdminController.php:1174
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d skipped."
|
msgid "%d skipped."
|
||||||
@@ -553,6 +561,7 @@ msgid_plural "%d skipped."
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. translators: %d: number of errors
|
||||||
#: src/Admin/AdminController.php:1182
|
#: src/Admin/AdminController.php:1182
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d error."
|
msgid "%d error."
|
||||||
@@ -997,6 +1006,7 @@ msgstr ""
|
|||||||
msgid "View in Licenses"
|
msgid "View in Licenses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: Link to licenses page
|
||||||
#: src/Admin/OrderLicenseController.php:280
|
#: src/Admin/OrderLicenseController.php:280
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "For more actions (revoke, extend, delete), go to the %s page."
|
msgid "For more actions (revoke, extend, delete), go to the %s page."
|
||||||
@@ -1133,8 +1143,8 @@ msgid ""
|
|||||||
"product settings override these defaults."
|
"product settings override these defaults."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:154
|
#: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:182
|
||||||
#: src/Product/LicensedProductType.php:420
|
#: src/Product/LicensedProductType.php:570
|
||||||
msgid "Max Activations"
|
msgid "Max Activations"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1142,7 +1152,7 @@ msgstr ""
|
|||||||
msgid "Default maximum number of domain activations per license."
|
msgid "Default maximum number of domain activations per license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:172
|
#: src/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:200
|
||||||
msgid "License Validity (Days)"
|
msgid "License Validity (Days)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1152,7 +1162,7 @@ msgid ""
|
|||||||
"lifetime licenses."
|
"lifetime licenses."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:190
|
#: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:218
|
||||||
msgid "Bind to Major Version"
|
msgid "Bind to Major Version"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1176,6 +1186,7 @@ msgstr ""
|
|||||||
msgid "Expiration Warning Schedule"
|
msgid "Expiration Warning Schedule"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: URL to WooCommerce email settings
|
||||||
#: src/Admin/SettingsController.php:230
|
#: src/Admin/SettingsController.php:230
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1299,6 +1310,7 @@ msgstr ""
|
|||||||
msgid "Each license requires a unique domain."
|
msgid "Each license requires a unique domain."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %d: license number
|
||||||
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
#: src/Checkout/CheckoutBlocksIntegration.php:129
|
||||||
#: src/Checkout/CheckoutController.php:224
|
#: src/Checkout/CheckoutController.php:224
|
||||||
#, php-format
|
#, php-format
|
||||||
@@ -1310,6 +1322,16 @@ msgstr ""
|
|||||||
msgid "required"
|
msgid "required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Checkout/CheckoutController.php:215
|
||||||
|
#, php-format
|
||||||
|
msgid "licensed_domains[%s][%d]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Checkout/CheckoutController.php:216
|
||||||
|
#, php-format
|
||||||
|
msgid "licensed_domain_%s_%d"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/Checkout/CheckoutController.php:323
|
#: src/Checkout/CheckoutController.php:323
|
||||||
msgid "Please enter a domain for your license."
|
msgid "Please enter a domain for your license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1318,16 +1340,19 @@ msgstr ""
|
|||||||
msgid "Please enter a valid domain for your license."
|
msgid "Please enter a valid domain for your license."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: license number
|
||||||
#: src/Checkout/CheckoutController.php:356
|
#: 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 ""
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: license number
|
||||||
#: src/Checkout/CheckoutController.php:371
|
#: 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 ""
|
||||||
|
|
||||||
|
#. translators: 1: domain name, 2: product name
|
||||||
#: src/Checkout/CheckoutController.php:385
|
#: src/Checkout/CheckoutController.php:385
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -1389,103 +1414,109 @@ msgstr ""
|
|||||||
msgid "Attachment file not found."
|
msgid "Attachment file not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: 1: provided hash, 2: calculated hash
|
||||||
#: src/Product/VersionManager.php:177
|
#: src/Product/VersionManager.php:177
|
||||||
#, php-format
|
#, php-format
|
||||||
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
|
#: src/Product/LicensedProductVariation.php:198
|
||||||
msgid "Monthly"
|
msgid "Monthly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:147
|
#: src/Product/LicensedProductVariation.php:202
|
||||||
msgid "Quarterly"
|
msgid "Quarterly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:151
|
#: src/Product/LicensedProductVariation.php:206
|
||||||
msgid "Yearly"
|
msgid "Yearly"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductVariation.php:156
|
#. translators: %d: number of days
|
||||||
|
#: src/Product/LicensedProductVariation.php:211
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d day"
|
msgid "%d day"
|
||||||
msgid_plural "%d days"
|
msgid_plural "%d days"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:72
|
#: src/Product/LicensedProductType.php:82
|
||||||
msgid "Licensed Product"
|
msgid "Licensed Product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:73
|
#: src/Product/LicensedProductType.php:83
|
||||||
msgid "Licensed Variable Product"
|
msgid "Licensed Variable Product"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:108
|
#: src/Product/LicensedProductType.php:136
|
||||||
msgid "License Settings"
|
msgid "License Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:135
|
#: src/Product/LicensedProductType.php:163
|
||||||
#: src/Product/LicensedProductType.php:402
|
#: src/Product/LicensedProductType.php:552
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%d days"
|
msgid "%d days"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:145
|
#. translators: %s: URL to settings page
|
||||||
|
#: src/Product/LicensedProductType.php:173
|
||||||
#, 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:147
|
#: src/Product/LicensedProductType.php:175
|
||||||
msgid "WooCommerce > Settings > Licensed Products"
|
msgid "WooCommerce > Settings > Licensed Products"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:157
|
#. translators: %d: default max activations value
|
||||||
|
#: src/Product/LicensedProductType.php:185
|
||||||
#, 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:175
|
#. translators: %s: default validity value
|
||||||
|
#: src/Product/LicensedProductType.php:203
|
||||||
#, 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:193
|
#. translators: %s: default bind to version value (Yes/No)
|
||||||
|
#: src/Product/LicensedProductType.php:221
|
||||||
#, 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:194
|
#: src/Product/LicensedProductType.php:222
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:194
|
#: src/Product/LicensedProductType.php:222
|
||||||
msgid "No"
|
msgid "No"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:329
|
#: src/Product/LicensedProductType.php:447
|
||||||
msgid "Version:"
|
msgid "Version:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:373
|
#: src/Product/LicensedProductType.php:523
|
||||||
msgid "Licensed products are always virtual"
|
msgid "Licensed products are always virtual"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:375
|
#: src/Product/LicensedProductType.php:525
|
||||||
msgid "Virtual"
|
msgid "Virtual"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:408
|
#: src/Product/LicensedProductType.php:558
|
||||||
msgid "License Duration (Days)"
|
msgid "License Duration (Days)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:417
|
#: src/Product/LicensedProductType.php:567
|
||||||
msgid "Leave empty for parent default. 0 = Lifetime."
|
msgid "Leave empty for parent default. 0 = Lifetime."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/Product/LicensedProductType.php:429
|
#: src/Product/LicensedProductType.php:579
|
||||||
msgid "Leave empty for parent default."
|
msgid "Leave empty for parent default."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1548,6 +1579,7 @@ msgstr ""
|
|||||||
msgid "You have no licenses yet."
|
msgid "You have no licenses yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: order number
|
||||||
#: src/Frontend/AccountController.php:245
|
#: src/Frontend/AccountController.php:245
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Order #%s"
|
msgid "Order #%s"
|
||||||
@@ -1697,6 +1729,7 @@ msgid ""
|
|||||||
"expiration date."
|
"expiration date."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: list of placeholders
|
||||||
#: src/Email/LicenseExpirationEmail.php:301
|
#: src/Email/LicenseExpirationEmail.php:301
|
||||||
#: src/Email/LicenseExpiredEmail.php:288
|
#: src/Email/LicenseExpiredEmail.php:288
|
||||||
#, php-format
|
#, php-format
|
||||||
@@ -1827,6 +1860,7 @@ msgstr ""
|
|||||||
msgid "Configure License"
|
msgid "Configure License"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %s: WooCommerce plugin name
|
||||||
#: wc-licensed-product.php:61
|
#: wc-licensed-product.php:61
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "%s requires WooCommerce to be installed and active."
|
msgid "%s requires WooCommerce to be installed and active."
|
||||||
@@ -1835,3 +1869,47 @@ msgstr ""
|
|||||||
#: wc-licensed-product.php:119
|
#: wc-licensed-product.php:119
|
||||||
msgid "WC Licensed Product requires WooCommerce to be installed and active."
|
msgid "WC Licensed Product requires WooCommerce to be installed and active."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Api/UpdateController.php:175
|
||||||
|
msgid "License validation failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Api/UpdateController.php:185
|
||||||
|
msgid "License not found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Api/UpdateController.php:195
|
||||||
|
msgid "Licensed product not found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Api/UpdateController.php:207
|
||||||
|
msgid "No versions available for this product."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Update/PluginUpdateChecker.php:295
|
||||||
|
msgid "WooCommerce plugin for selling licensed software products with domain-bound license keys."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:163
|
||||||
|
msgid "Auto-Updates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:165
|
||||||
|
msgid "Configure automatic plugin updates from the license server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:169
|
||||||
|
msgid "Enable Auto-Updates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:172
|
||||||
|
msgid "Automatically check for and receive plugin updates from the license server."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:177
|
||||||
|
msgid "Check Frequency (Hours)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/Admin/SettingsController.php:180
|
||||||
|
msgid "How often to check for updates (in hours)."
|
||||||
|
msgstr ""
|
||||||
|
|||||||
272
openapi.json
272
openapi.json
@@ -3,7 +3,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "WooCommerce Licensed Product API",
|
"title": "WooCommerce Licensed Product API",
|
||||||
"description": "REST API for validating and managing software licenses bound to domains. This API allows external applications to validate license keys, check license status, and activate licenses on specific domains.\n\n## Response Signing (Optional)\n\nWhen the server is configured with `WC_LICENSE_SERVER_SECRET`, all API responses include cryptographic signatures for tamper protection:\n\n- `X-License-Signature`: HMAC-SHA256 signature of the response\n- `X-License-Timestamp`: Unix timestamp when the response was generated\n\nSignature verification prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` library's `SecureLicenseClient` class to automatically verify signatures.",
|
"description": "REST API for validating and managing software licenses bound to domains. This API allows external applications to validate license keys, check license status, and activate licenses on specific domains.\n\n## Response Signing (Optional)\n\nWhen the server is configured with `WC_LICENSE_SERVER_SECRET`, all API responses include cryptographic signatures for tamper protection:\n\n- `X-License-Signature`: HMAC-SHA256 signature of the response\n- `X-License-Timestamp`: Unix timestamp when the response was generated\n\nSignature verification prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` library's `SecureLicenseClient` class to automatically verify signatures.",
|
||||||
"version": "0.3.2",
|
"version": "0.6.0",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "Marco Graetsch",
|
"name": "Marco Graetsch",
|
||||||
"url": "https://src.bundespruefstelle.ch/magdev",
|
"url": "https://src.bundespruefstelle.ch/magdev",
|
||||||
@@ -332,6 +332,148 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/update-check": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "checkForUpdates",
|
||||||
|
"summary": "Check for plugin updates",
|
||||||
|
"description": "Checks if a newer version of the licensed product is available. Returns WordPress-compatible update information that can be used to integrate with WordPress's native plugin update system.",
|
||||||
|
"tags": ["Plugin Updates"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateCheckRequest"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"license_key": "ABCD-1234-EFGH-5678",
|
||||||
|
"domain": "example.com",
|
||||||
|
"plugin_slug": "my-licensed-plugin",
|
||||||
|
"current_version": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateCheckRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Update check completed successfully",
|
||||||
|
"headers": {
|
||||||
|
"X-License-Signature": {
|
||||||
|
"$ref": "#/components/headers/X-License-Signature"
|
||||||
|
},
|
||||||
|
"X-License-Timestamp": {
|
||||||
|
"$ref": "#/components/headers/X-License-Timestamp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateCheckResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"update_available": {
|
||||||
|
"summary": "Update is available",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"update_available": true,
|
||||||
|
"version": "1.2.0",
|
||||||
|
"slug": "my-licensed-plugin",
|
||||||
|
"plugin": "my-licensed-plugin/my-licensed-plugin.php",
|
||||||
|
"download_url": "https://example.com/license-download/123-456-abc123",
|
||||||
|
"package": "https://example.com/license-download/123-456-abc123",
|
||||||
|
"last_updated": "2026-01-27",
|
||||||
|
"tested": "6.7",
|
||||||
|
"requires": "6.0",
|
||||||
|
"requires_php": "8.3",
|
||||||
|
"changelog": "## 1.2.0\n- New feature added\n- Bug fixes",
|
||||||
|
"package_hash": "sha256:abc123def456...",
|
||||||
|
"name": "My Licensed Plugin",
|
||||||
|
"homepage": "https://example.com/product/my-plugin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"no_update": {
|
||||||
|
"summary": "No update available",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"update_available": false,
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "License validation failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"license_invalid": {
|
||||||
|
"summary": "License is not valid",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "license_invalid",
|
||||||
|
"message": "License validation failed."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain_mismatch": {
|
||||||
|
"summary": "Domain mismatch",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "domain_mismatch",
|
||||||
|
"message": "This license is not valid for this domain."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "License or product not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"license_not_found": {
|
||||||
|
"summary": "License not found",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "license_not_found",
|
||||||
|
"message": "License not found."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"product_not_found": {
|
||||||
|
"summary": "Product not found",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"update_available": false,
|
||||||
|
"error": "product_not_found",
|
||||||
|
"message": "Licensed product not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"$ref": "#/components/responses/RateLimitExceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -516,6 +658,130 @@
|
|||||||
"description": "Seconds until rate limit resets"
|
"description": "Seconds until rate limit resets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"UpdateCheckRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["license_key", "domain"],
|
||||||
|
"properties": {
|
||||||
|
"license_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The license key to validate (format: XXXX-XXXX-XXXX-XXXX)",
|
||||||
|
"maxLength": 64,
|
||||||
|
"example": "ABCD-1234-EFGH-5678"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The domain the plugin is installed on",
|
||||||
|
"maxLength": 255,
|
||||||
|
"example": "example.com"
|
||||||
|
},
|
||||||
|
"plugin_slug": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The plugin slug (optional, for identification)",
|
||||||
|
"example": "my-licensed-plugin"
|
||||||
|
},
|
||||||
|
"current_version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Currently installed version for comparison",
|
||||||
|
"example": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UpdateCheckResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the request was successful"
|
||||||
|
},
|
||||||
|
"update_available": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether an update is available"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Latest available version"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Plugin slug for WordPress"
|
||||||
|
},
|
||||||
|
"plugin": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Plugin basename (slug/slug.php)"
|
||||||
|
},
|
||||||
|
"download_url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Secure download URL for the update package"
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Alias for download_url (WordPress compatibility)"
|
||||||
|
},
|
||||||
|
"last_updated": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"description": "Date of the latest release"
|
||||||
|
},
|
||||||
|
"tested": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Highest WordPress version tested with"
|
||||||
|
},
|
||||||
|
"requires": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Minimum required WordPress version"
|
||||||
|
},
|
||||||
|
"requires_php": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Minimum required PHP version"
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Release notes/changelog for the update"
|
||||||
|
},
|
||||||
|
"package_hash": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SHA256 hash of the package for integrity verification",
|
||||||
|
"example": "sha256:abc123..."
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Product name"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "Product homepage URL"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Plugin icons for WordPress admin",
|
||||||
|
"properties": {
|
||||||
|
"1x": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"2x": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sections": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Content sections for plugin info modal",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"changelog": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -577,6 +843,10 @@
|
|||||||
{
|
{
|
||||||
"name": "License Activation",
|
"name": "License Activation",
|
||||||
"description": "Activate licenses on domains"
|
"description": "Activate licenses on domains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Plugin Updates",
|
||||||
|
"description": "Check for plugin updates via WordPress-compatible API"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
releases/wc-licensed-product-0.5.10.zip
Normal file
BIN
releases/wc-licensed-product-0.5.10.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.10.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.10.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2bbc0655f724e201367247f0e40974ddce6d7c559987e661f2b06b43294fc99f wc-licensed-product-0.5.10.zip
|
||||||
BIN
releases/wc-licensed-product-0.5.11.zip
Normal file
BIN
releases/wc-licensed-product-0.5.11.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.11.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.11.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
32571178bfa8f0d0a03ed05b498d5f9b3c860104393a96732e86a03b6de298d2 wc-licensed-product-0.5.11.zip
|
||||||
1
releases/wc-licensed-product-0.5.12.sha256
Normal file
1
releases/wc-licensed-product-0.5.12.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
20bb5cd453de9bca781864430ebd152c82f660b6f9fc3f09107ba03489a71d75 /home/magdev/workspaces/php/wordpress/wp-content/plugins/wc-licensed-product/releases/wc-licensed-product-0.5.12.zip
|
||||||
BIN
releases/wc-licensed-product-0.5.12.zip
Normal file
BIN
releases/wc-licensed-product-0.5.12.zip
Normal file
Binary file not shown.
BIN
releases/wc-licensed-product-0.5.13.zip
Normal file
BIN
releases/wc-licensed-product-0.5.13.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.13.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.13.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
814710ad899529d0015494e4b332eace7d8e55aeda381fdf61f99274c0bf910c wc-licensed-product-0.5.13.zip
|
||||||
1
releases/wc-licensed-product-0.5.15.sha256
Normal file
1
releases/wc-licensed-product-0.5.15.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
47407de49bae4c649644af64e87b44b32fb30eeb2d50890ff8c4bbb741059278 wc-licensed-product-0.5.15.zip
|
||||||
BIN
releases/wc-licensed-product-0.5.8.zip
Normal file
BIN
releases/wc-licensed-product-0.5.8.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.8.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.8.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
670c2f5182ea7140ccf9533c2b4179daf7890019a244973f467f2a5c7622b9f4 wc-licensed-product-0.5.8.zip
|
||||||
BIN
releases/wc-licensed-product-0.5.9.zip
Normal file
BIN
releases/wc-licensed-product-0.5.9.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.5.9.zip.sha256
Normal file
1
releases/wc-licensed-product-0.5.9.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
fae77dab56cb8f46693cf44fe6a1dc38ad0526d881cab2cd1f0878b234afaa8b wc-licensed-product-0.5.9.zip
|
||||||
@@ -83,7 +83,7 @@ final class OrderLicenseController
|
|||||||
$hasLicensedProduct = false;
|
$hasLicensedProduct = false;
|
||||||
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)) {
|
||||||
$hasLicensedProduct = true;
|
$hasLicensedProduct = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ final class OrderLicenseController
|
|||||||
// Legacy: one license per licensed product
|
// Legacy: one license per 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)) {
|
||||||
$expectedLicenses++;
|
$expectedLicenses++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,7 +567,7 @@ final class OrderLicenseController
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +615,7 @@ final class OrderLicenseController
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ final class SettingsController
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'' => __('Plugin License', 'wc-licensed-product'),
|
'' => __('Plugin License', 'wc-licensed-product'),
|
||||||
|
'auto-updates' => __('Auto-Updates', 'wc-licensed-product'),
|
||||||
'defaults' => __('Default Settings', 'wc-licensed-product'),
|
'defaults' => __('Default Settings', 'wc-licensed-product'),
|
||||||
'notifications' => __('Notifications', 'wc-licensed-product'),
|
'notifications' => __('Notifications', 'wc-licensed-product'),
|
||||||
];
|
];
|
||||||
@@ -112,6 +113,7 @@ final class SettingsController
|
|||||||
$currentSection = $this->getCurrentSection();
|
$currentSection = $this->getCurrentSection();
|
||||||
|
|
||||||
return match ($currentSection) {
|
return match ($currentSection) {
|
||||||
|
'auto-updates' => $this->getAutoUpdatesSettings(),
|
||||||
'defaults' => $this->getDefaultsSettings(),
|
'defaults' => $this->getDefaultsSettings(),
|
||||||
'notifications' => $this->getNotificationsSettings(),
|
'notifications' => $this->getNotificationsSettings(),
|
||||||
default => $this->getPluginLicenseSettings(),
|
default => $this->getPluginLicenseSettings(),
|
||||||
@@ -160,6 +162,44 @@ final class SettingsController
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get auto-updates settings
|
||||||
|
*/
|
||||||
|
private function getAutoUpdatesSettings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'auto_update_section_title' => [
|
||||||
|
'name' => __('Auto-Updates', 'wc-licensed-product'),
|
||||||
|
'type' => 'title',
|
||||||
|
'desc' => __('Configure automatic plugin updates from the license server.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_section_auto_update',
|
||||||
|
],
|
||||||
|
'plugin_auto_update_enabled' => [
|
||||||
|
'name' => __('Enable Auto-Updates', 'wc-licensed-product'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'desc' => __('Automatically check for and receive plugin updates from the license server.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_plugin_auto_update_enabled',
|
||||||
|
'default' => 'yes',
|
||||||
|
],
|
||||||
|
'update_check_frequency' => [
|
||||||
|
'name' => __('Check Frequency (Hours)', 'wc-licensed-product'),
|
||||||
|
'type' => 'number',
|
||||||
|
'desc' => __('How often to check for updates (in hours).', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_update_check_frequency',
|
||||||
|
'default' => '12',
|
||||||
|
'custom_attributes' => [
|
||||||
|
'min' => '1',
|
||||||
|
'max' => '168',
|
||||||
|
'step' => '1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'auto_update_section_end' => [
|
||||||
|
'type' => 'sectionend',
|
||||||
|
'id' => 'wc_licensed_product_section_auto_update_end',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get default license settings
|
* Get default license settings
|
||||||
*/
|
*/
|
||||||
@@ -460,6 +500,23 @@ final class SettingsController
|
|||||||
return !empty($secret) ? (string) $secret : null;
|
return !empty($secret) ? (string) $secret : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if auto-updates are enabled
|
||||||
|
*/
|
||||||
|
public static function isAutoUpdateEnabled(): bool
|
||||||
|
{
|
||||||
|
return get_option('wc_licensed_product_plugin_auto_update_enabled', 'yes') === 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update check frequency in hours
|
||||||
|
*/
|
||||||
|
public static function getUpdateCheckFrequency(): int
|
||||||
|
{
|
||||||
|
$value = get_option('wc_licensed_product_update_check_frequency', 12);
|
||||||
|
return max(1, min(168, (int) $value));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle AJAX verify license request
|
* Handle AJAX verify license request
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,25 +43,21 @@ final class VersionAdminController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add versions meta box to product edit page
|
* Add versions meta box to product edit page
|
||||||
|
* Always adds the meta box - visibility is controlled via CSS/JavaScript based on product type
|
||||||
*/
|
*/
|
||||||
public function addVersionsMetaBox(): void
|
public function addVersionsMetaBox(): void
|
||||||
{
|
{
|
||||||
global $post;
|
global $post;
|
||||||
|
|
||||||
// Only add meta box for licensed products or new products
|
|
||||||
if ($post && $post->post_type === 'product') {
|
if ($post && $post->post_type === 'product') {
|
||||||
$product = wc_get_product($post->ID);
|
add_meta_box(
|
||||||
// Show for licensed products or new products (where type might be selected later)
|
'wc_licensed_product_versions',
|
||||||
if (!$product || $product->is_type('licensed') || $post->post_status === 'auto-draft') {
|
__('Product Versions', 'wc-licensed-product'),
|
||||||
add_meta_box(
|
[$this, 'renderVersionsMetaBox'],
|
||||||
'wc_licensed_product_versions',
|
'product',
|
||||||
__('Product Versions', 'wc-licensed-product'),
|
'normal',
|
||||||
[$this, 'renderVersionsMetaBox'],
|
'high'
|
||||||
'product',
|
);
|
||||||
'normal',
|
|
||||||
'high'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,12 +276,13 @@ final class VersionAdminController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify product exists and is of type licensed
|
// Verify product exists and is of type licensed
|
||||||
$product = wc_get_product($productId);
|
// Use WC_Product_Factory::get_product_type() for reliable type detection
|
||||||
if (!$product) {
|
$productType = \WC_Product_Factory::get_product_type($productId);
|
||||||
|
if (!$productType) {
|
||||||
wp_send_json_error(['message' => __('Product not found.', 'wc-licensed-product')]);
|
wp_send_json_error(['message' => __('Product not found.', 'wc-licensed-product')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$product->is_type('licensed')) {
|
if (!in_array($productType, ['licensed', 'licensed-variable'], true)) {
|
||||||
wp_send_json_error(['message' => __('This product is not a licensed product.', 'wc-licensed-product')]);
|
wp_send_json_error(['message' => __('This product is not a licensed product.', 'wc-licensed-product')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
352
src/Api/UpdateController.php
Normal file
352
src/Api/UpdateController.php
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Update Controller
|
||||||
|
*
|
||||||
|
* REST API endpoint for plugin update checks
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Api;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||||
|
use Jeremias\WcLicensedProduct\Product\ProductVersion;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_REST_Server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles REST API endpoint for plugin update checks
|
||||||
|
*
|
||||||
|
* This endpoint allows licensed plugins to check for updates from this WooCommerce store.
|
||||||
|
* It validates the license and returns WordPress-compatible update information.
|
||||||
|
*/
|
||||||
|
final class UpdateController
|
||||||
|
{
|
||||||
|
private const NAMESPACE = 'wc-licensed-product/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default rate limit: requests per window per IP
|
||||||
|
*/
|
||||||
|
private const DEFAULT_RATE_LIMIT = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default rate limit window in seconds
|
||||||
|
*/
|
||||||
|
private const DEFAULT_RATE_WINDOW = 60;
|
||||||
|
|
||||||
|
private LicenseManager $licenseManager;
|
||||||
|
private VersionManager $versionManager;
|
||||||
|
|
||||||
|
public function __construct(LicenseManager $licenseManager, VersionManager $versionManager)
|
||||||
|
{
|
||||||
|
$this->licenseManager = $licenseManager;
|
||||||
|
$this->versionManager = $versionManager;
|
||||||
|
$this->registerHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register WordPress hooks
|
||||||
|
*/
|
||||||
|
private function registerHooks(): void
|
||||||
|
{
|
||||||
|
add_action('rest_api_init', [$this, 'registerRoutes']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configured rate limit (requests per window)
|
||||||
|
*/
|
||||||
|
private function getRateLimit(): int
|
||||||
|
{
|
||||||
|
return defined('WC_LICENSE_RATE_LIMIT')
|
||||||
|
? (int) WC_LICENSE_RATE_LIMIT
|
||||||
|
: self::DEFAULT_RATE_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configured rate limit window in seconds
|
||||||
|
*/
|
||||||
|
private function getRateWindow(): int
|
||||||
|
{
|
||||||
|
return defined('WC_LICENSE_RATE_WINDOW')
|
||||||
|
? (int) WC_LICENSE_RATE_WINDOW
|
||||||
|
: self::DEFAULT_RATE_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check rate limit for current IP
|
||||||
|
*
|
||||||
|
* @return WP_REST_Response|null Returns error response if rate limited, null if OK
|
||||||
|
*/
|
||||||
|
private function checkRateLimit(): ?WP_REST_Response
|
||||||
|
{
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
|
$transientKey = 'wclp_update_rate_' . md5($ip);
|
||||||
|
$rateLimit = $this->getRateLimit();
|
||||||
|
$rateWindow = $this->getRateWindow();
|
||||||
|
|
||||||
|
$data = get_transient($transientKey);
|
||||||
|
|
||||||
|
if ($data === false) {
|
||||||
|
set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = (int) ($data['count'] ?? 0);
|
||||||
|
$start = (int) ($data['start'] ?? time());
|
||||||
|
|
||||||
|
if (time() - $start >= $rateWindow) {
|
||||||
|
set_transient($transientKey, ['count' => 1, 'start' => time()], $rateWindow);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($count >= $rateLimit) {
|
||||||
|
$retryAfter = $rateWindow - (time() - $start);
|
||||||
|
$response = new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'rate_limit_exceeded',
|
||||||
|
'message' => __('Too many requests. Please try again later.', 'wc-licensed-product'),
|
||||||
|
'retry_after' => $retryAfter,
|
||||||
|
], 429);
|
||||||
|
$response->header('Retry-After', (string) $retryAfter);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient($transientKey, ['count' => $count + 1, 'start' => $start], $rateWindow);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API routes
|
||||||
|
*/
|
||||||
|
public function registerRoutes(): void
|
||||||
|
{
|
||||||
|
register_rest_route(self::NAMESPACE, '/update-check', [
|
||||||
|
'methods' => WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => [$this, 'handleUpdateCheck'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
'args' => [
|
||||||
|
'license_key' => [
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => function ($value): bool {
|
||||||
|
$len = strlen($value);
|
||||||
|
return !empty($value) && $len >= 8 && $len <= 64;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'domain' => [
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
'validate_callback' => function ($value): bool {
|
||||||
|
return !empty($value) && strlen($value) <= 255;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'plugin_slug' => [
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
],
|
||||||
|
'current_version' => [
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle update check request
|
||||||
|
*/
|
||||||
|
public function handleUpdateCheck(WP_REST_Request $request): WP_REST_Response
|
||||||
|
{
|
||||||
|
$rateLimitResponse = $this->checkRateLimit();
|
||||||
|
if ($rateLimitResponse !== null) {
|
||||||
|
return $rateLimitResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
$licenseKey = $request->get_param('license_key');
|
||||||
|
$domain = $request->get_param('domain');
|
||||||
|
$currentVersion = $request->get_param('current_version');
|
||||||
|
|
||||||
|
// Validate license
|
||||||
|
$validationResult = $this->licenseManager->validateLicense($licenseKey, $domain);
|
||||||
|
|
||||||
|
if (!$validationResult['valid']) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'update_available' => false,
|
||||||
|
'error' => $validationResult['error'] ?? 'license_invalid',
|
||||||
|
'message' => $validationResult['message'] ?? __('License validation failed.', 'wc-licensed-product'),
|
||||||
|
], $validationResult['error'] === 'license_not_found' ? 404 : 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get license to access product ID
|
||||||
|
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
||||||
|
if (!$license) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'update_available' => false,
|
||||||
|
'error' => 'license_not_found',
|
||||||
|
'message' => __('License not found.', 'wc-licensed-product'),
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$productId = $license->getProductId();
|
||||||
|
$product = wc_get_product($productId);
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => false,
|
||||||
|
'update_available' => false,
|
||||||
|
'error' => 'product_not_found',
|
||||||
|
'message' => __('Licensed product not found.', 'wc-licensed-product'),
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get latest version based on major version binding
|
||||||
|
$latestVersion = $this->getLatestVersionForLicense($license);
|
||||||
|
|
||||||
|
if (!$latestVersion) {
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => false,
|
||||||
|
'version' => $currentVersion ?? '0.0.0',
|
||||||
|
'message' => __('No versions available for this product.', 'wc-licensed-product'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if update is available
|
||||||
|
$updateAvailable = $currentVersion
|
||||||
|
? version_compare($latestVersion->getVersion(), $currentVersion, '>')
|
||||||
|
: true;
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
$response = $this->buildUpdateResponse($product, $latestVersion, $license, $updateAvailable);
|
||||||
|
|
||||||
|
return new WP_REST_Response($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latest version for a license, respecting major version binding
|
||||||
|
*/
|
||||||
|
private function getLatestVersionForLicense($license): ?ProductVersion
|
||||||
|
{
|
||||||
|
$productId = $license->getProductId();
|
||||||
|
|
||||||
|
// Check if license is bound to a specific version
|
||||||
|
$versionId = $license->getVersionId();
|
||||||
|
if ($versionId) {
|
||||||
|
$boundVersion = $this->versionManager->getVersionById($versionId);
|
||||||
|
if ($boundVersion) {
|
||||||
|
// Get latest version for this major version
|
||||||
|
return $this->versionManager->getLatestVersionForMajor(
|
||||||
|
$productId,
|
||||||
|
$boundVersion->getMajorVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No version binding, return latest overall
|
||||||
|
return $this->versionManager->getLatestVersion($productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build WordPress-compatible update response
|
||||||
|
*/
|
||||||
|
private function buildUpdateResponse($product, ProductVersion $version, $license, bool $updateAvailable): array
|
||||||
|
{
|
||||||
|
$productSlug = $product->get_slug();
|
||||||
|
|
||||||
|
// Generate secure download URL
|
||||||
|
$downloadUrl = $this->generateUpdateDownloadUrl($license->getId(), $version->getId());
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'update_available' => $updateAvailable,
|
||||||
|
'version' => $version->getVersion(),
|
||||||
|
'slug' => $productSlug,
|
||||||
|
'plugin' => $productSlug . '/' . $productSlug . '.php',
|
||||||
|
'download_url' => $downloadUrl,
|
||||||
|
'package' => $downloadUrl,
|
||||||
|
'last_updated' => $version->getReleasedAt()->format('Y-m-d'),
|
||||||
|
'tested' => $this->getTestedWpVersion(),
|
||||||
|
'requires' => $this->getRequiredWpVersion(),
|
||||||
|
'requires_php' => $this->getRequiredPhpVersion(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add changelog if available
|
||||||
|
if ($version->getReleaseNotes()) {
|
||||||
|
$response['changelog'] = $version->getReleaseNotes();
|
||||||
|
$response['sections'] = [
|
||||||
|
'description' => $product->get_short_description() ?: $product->get_description(),
|
||||||
|
'changelog' => $version->getReleaseNotes(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add package hash for integrity verification
|
||||||
|
if ($version->getFileHash()) {
|
||||||
|
$response['package_hash'] = 'sha256:' . $version->getFileHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add product name and homepage
|
||||||
|
$response['name'] = $product->get_name();
|
||||||
|
$response['homepage'] = get_permalink($product->get_id());
|
||||||
|
|
||||||
|
// Add icons if product has featured image
|
||||||
|
$imageId = $product->get_image_id();
|
||||||
|
if ($imageId) {
|
||||||
|
$iconUrl = wp_get_attachment_image_url($imageId, 'thumbnail');
|
||||||
|
$iconUrl2x = wp_get_attachment_image_url($imageId, 'medium');
|
||||||
|
if ($iconUrl) {
|
||||||
|
$response['icons'] = [
|
||||||
|
'1x' => $iconUrl,
|
||||||
|
'2x' => $iconUrl2x ?: $iconUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate secure download URL for updates
|
||||||
|
*/
|
||||||
|
private function generateUpdateDownloadUrl(int $licenseId, int $versionId): string
|
||||||
|
{
|
||||||
|
$data = $licenseId . '-' . $versionId . '-' . wp_salt('auth');
|
||||||
|
$hash = substr(hash('sha256', $data), 0, 16);
|
||||||
|
$downloadKey = $licenseId . '-' . $versionId . '-' . $hash;
|
||||||
|
|
||||||
|
return home_url('license-download/' . $downloadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tested WordPress version from plugin headers
|
||||||
|
*/
|
||||||
|
private function getTestedWpVersion(): string
|
||||||
|
{
|
||||||
|
return get_option('wc_licensed_product_tested_wp', '6.7');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get required WordPress version from plugin headers
|
||||||
|
*/
|
||||||
|
private function getRequiredWpVersion(): string
|
||||||
|
{
|
||||||
|
return get_option('wc_licensed_product_requires_wp', '6.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get required PHP version
|
||||||
|
*/
|
||||||
|
private function getRequiredPhpVersion(): string
|
||||||
|
{
|
||||||
|
return get_option('wc_licensed_product_requires_php', '8.3');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -112,10 +112,12 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
|||||||
public function get_script_data(): array
|
public function get_script_data(): array
|
||||||
{
|
{
|
||||||
$isMultiDomain = SettingsController::isMultiDomainEnabled();
|
$isMultiDomain = SettingsController::isMultiDomainEnabled();
|
||||||
|
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||||
|
$hasLicensedProducts = !empty($licensedProducts);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'hasLicensedProducts' => $this->cartHasLicensedProducts(),
|
'hasLicensedProducts' => $hasLicensedProducts,
|
||||||
'licensedProducts' => $this->getLicensedProductsFromCart(),
|
'licensedProducts' => $licensedProducts,
|
||||||
'isMultiDomainEnabled' => $isMultiDomain,
|
'isMultiDomainEnabled' => $isMultiDomain,
|
||||||
'fieldPlaceholder' => __('example.com', 'wc-licensed-product'),
|
'fieldPlaceholder' => __('example.com', 'wc-licensed-product'),
|
||||||
'fieldDescription' => $isMultiDomain
|
'fieldDescription' => $isMultiDomain
|
||||||
@@ -151,8 +153,11 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$licensedProducts = [];
|
$licensedProducts = [];
|
||||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
$cartContents = WC()->cart->get_cart();
|
||||||
|
|
||||||
|
foreach ($cartContents as $cartKey => $cartItem) {
|
||||||
$product = $cartItem['data'];
|
$product = $cartItem['data'];
|
||||||
|
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -171,11 +176,12 @@ final class CheckoutBlocksIntegration implements IntegrationInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for variations of licensed-variable products
|
// Check for variations of licensed-variable products
|
||||||
if ($product->is_type('variation')) {
|
// Use WC_Product_Factory::get_product_type() for reliable parent type check
|
||||||
$parentId = $product->get_parent_id();
|
$parentId = $product->get_parent_id();
|
||||||
$parent = wc_get_product($parentId);
|
if ($parentId) {
|
||||||
|
$parentType = \WC_Product_Factory::get_product_type($parentId);
|
||||||
|
|
||||||
if ($parent && $parent->is_type('licensed-variable')) {
|
if ($parentType === 'licensed-variable') {
|
||||||
$variationId = $product->get_id();
|
$variationId = $product->get_id();
|
||||||
|
|
||||||
// Get duration label if it's a LicensedProductVariation
|
// Get duration label if it's a LicensedProductVariation
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ final class CheckoutController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$licensedProducts = [];
|
$licensedProducts = [];
|
||||||
foreach (WC()->cart->get_cart() as $cartItem) {
|
foreach (WC()->cart->get_cart() as $cartItemKey => $cartItem) {
|
||||||
$product = $cartItem['data'];
|
$product = $cartItem['data'];
|
||||||
|
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -87,11 +88,12 @@ final class CheckoutController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for variations of licensed-variable products
|
// Check for variations of licensed-variable products
|
||||||
if ($product->is_type('variation')) {
|
// Use WC_Product_Factory::get_product_type() for reliable parent type check
|
||||||
$parentId = $product->get_parent_id();
|
$parentId = $product->get_parent_id();
|
||||||
$parent = wc_get_product($parentId);
|
if ($parentId) {
|
||||||
|
$parentType = \WC_Product_Factory::get_product_type($parentId);
|
||||||
|
|
||||||
if ($parent && $parent->is_type('licensed-variable')) {
|
if ($parentType === 'licensed-variable') {
|
||||||
$variationId = $product->get_id();
|
$variationId = $product->get_id();
|
||||||
// Use combination key to allow same product with different variations
|
// Use combination key to allow same product with different variations
|
||||||
$key = "{$parentId}_{$variationId}";
|
$key = "{$parentId}_{$variationId}";
|
||||||
@@ -127,6 +129,7 @@ final class CheckoutController
|
|||||||
public function addDomainField(): void
|
public function addDomainField(): void
|
||||||
{
|
{
|
||||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||||
|
|
||||||
if (empty($licensedProducts)) {
|
if (empty($licensedProducts)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -401,6 +404,7 @@ final class CheckoutController
|
|||||||
public function saveDomainField(int $orderId): void
|
public function saveDomainField(int $orderId): void
|
||||||
{
|
{
|
||||||
$licensedProducts = $this->getLicensedProductsFromCart();
|
$licensedProducts = $this->getLicensedProductsFromCart();
|
||||||
|
|
||||||
if (empty($licensedProducts)) {
|
if (empty($licensedProducts)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ final class Installer
|
|||||||
{
|
{
|
||||||
self::createTables();
|
self::createTables();
|
||||||
self::createCacheDir();
|
self::createCacheDir();
|
||||||
|
self::registerProductTypes();
|
||||||
|
|
||||||
// Set version in options
|
// Set version in options
|
||||||
update_option('wc_licensed_product_version', WC_LICENSED_PRODUCT_VERSION);
|
update_option('wc_licensed_product_version', WC_LICENSED_PRODUCT_VERSION);
|
||||||
@@ -43,6 +44,28 @@ final class Installer
|
|||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom product type terms in the product_type taxonomy
|
||||||
|
* This is required for WC_Product_Factory::get_product_type() to work correctly
|
||||||
|
*/
|
||||||
|
public static function registerProductTypes(): void
|
||||||
|
{
|
||||||
|
// Ensure WooCommerce taxonomies are registered
|
||||||
|
if (!taxonomy_exists('product_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 'licensed' product type term if it doesn't exist
|
||||||
|
if (!term_exists('licensed', 'product_type')) {
|
||||||
|
wp_insert_term('licensed', 'product_type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 'licensed-variable' product type term if it doesn't exist
|
||||||
|
if (!term_exists('licensed-variable', 'product_type')) {
|
||||||
|
wp_insert_term('licensed-variable', 'product_type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run on plugin deactivation
|
* Run on plugin deactivation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,10 +37,18 @@ class LicenseManager
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for our custom variation class
|
||||||
|
if ($product instanceof LicensedProductVariation) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Variation of a licensed-variable product
|
// Variation of a licensed-variable product
|
||||||
if ($product->is_type('variation') && $product->get_parent_id()) {
|
// Use WC_Product_Factory::get_product_type() for reliable parent type check
|
||||||
$parent = wc_get_product($product->get_parent_id());
|
// This queries the database directly and doesn't depend on product class loading
|
||||||
if ($parent && $parent->is_type('licensed-variable')) {
|
$parentId = $product->get_parent_id();
|
||||||
|
if ($parentId) {
|
||||||
|
$parentType = \WC_Product_Factory::get_product_type($parentId);
|
||||||
|
if ($parentType === 'licensed-variable') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,10 +109,10 @@ class LicenseManager
|
|||||||
// For variations, load the variation; otherwise load the parent product
|
// For variations, load the variation; otherwise load the parent product
|
||||||
if ($variationId) {
|
if ($variationId) {
|
||||||
$settingsProduct = wc_get_product($variationId);
|
$settingsProduct = wc_get_product($variationId);
|
||||||
$parentProduct = wc_get_product($productId);
|
|
||||||
|
|
||||||
// Verify parent is licensed-variable
|
// Verify parent is licensed-variable using DB-level type check
|
||||||
if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
|
$parentType = \WC_Product_Factory::get_product_type($productId);
|
||||||
|
if ($parentType !== 'licensed-variable') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
|||||||
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
||||||
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
|
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
|
||||||
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
||||||
|
use Jeremias\WcLicensedProduct\Api\UpdateController;
|
||||||
use Jeremias\WcLicensedProduct\Checkout\CheckoutBlocksIntegration;
|
use Jeremias\WcLicensedProduct\Checkout\CheckoutBlocksIntegration;
|
||||||
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
|
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
|
||||||
use Jeremias\WcLicensedProduct\Checkout\StoreApiExtension;
|
use Jeremias\WcLicensedProduct\Checkout\StoreApiExtension;
|
||||||
@@ -27,6 +28,7 @@ use Jeremias\WcLicensedProduct\License\LicenseManager;
|
|||||||
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
||||||
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
use Jeremias\WcLicensedProduct\Product\LicensedProductType;
|
||||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||||
|
use Jeremias\WcLicensedProduct\Update\PluginUpdateChecker;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Loader\FilesystemLoader;
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
@@ -139,8 +141,9 @@ final class Plugin
|
|||||||
new AccountController($this->twig, $this->licenseManager, $this->versionManager, $this->downloadController);
|
new AccountController($this->twig, $this->licenseManager, $this->versionManager, $this->downloadController);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always initialize REST API and email controller
|
// Always initialize REST API, update API, and email controller
|
||||||
new RestApiController($this->licenseManager);
|
new RestApiController($this->licenseManager);
|
||||||
|
new UpdateController($this->licenseManager, $this->versionManager);
|
||||||
new LicenseEmailController($this->licenseManager);
|
new LicenseEmailController($this->licenseManager);
|
||||||
|
|
||||||
// Initialize response signing if server secret is configured
|
// Initialize response signing if server secret is configured
|
||||||
@@ -162,6 +165,12 @@ final class Plugin
|
|||||||
add_action('admin_notices', [$this, 'showUnlicensedNotice']);
|
add_action('admin_notices', [$this, 'showUnlicensedNotice']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize update checker if license server is configured (client-side updates)
|
||||||
|
$serverUrl = SettingsController::getPluginLicenseServerUrl();
|
||||||
|
if (!empty($serverUrl) && !$licenseChecker->isSelfLicensing()) {
|
||||||
|
PluginUpdateChecker::getInstance()->register();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,6 +219,7 @@ final class Plugin
|
|||||||
|
|
||||||
// Try new multi-domain format first
|
// Try new multi-domain format first
|
||||||
$domainData = $order->get_meta('_licensed_product_domains');
|
$domainData = $order->get_meta('_licensed_product_domains');
|
||||||
|
|
||||||
if (!empty($domainData) && is_array($domainData)) {
|
if (!empty($domainData) && is_array($domainData)) {
|
||||||
$this->generateLicensesMultiDomain($order, $domainData);
|
$this->generateLicensesMultiDomain($order, $domainData);
|
||||||
return;
|
return;
|
||||||
@@ -244,7 +254,12 @@ final class Plugin
|
|||||||
// 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 || !$this->licenseManager->isLicensedProduct($product)) {
|
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->licenseManager->isLicensedProduct($product)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,12 +293,14 @@ final class Plugin
|
|||||||
private function generateLicensesSingleDomain(\WC_Order $order): void
|
private function generateLicensesSingleDomain(\WC_Order $order): void
|
||||||
{
|
{
|
||||||
$domain = $order->get_meta('_licensed_product_domain');
|
$domain = $order->get_meta('_licensed_product_domain');
|
||||||
|
|
||||||
if (empty($domain)) {
|
if (empty($domain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($order->get_items() as $item) {
|
foreach ($order->get_items() as $item) {
|
||||||
$product = $item->get_product();
|
$product = $item->get_product();
|
||||||
|
|
||||||
if ($product && $this->licenseManager->isLicensedProduct($product)) {
|
if ($product && $this->licenseManager->isLicensedProduct($product)) {
|
||||||
// Get the parent product ID (for variations, this is the main 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();
|
$productId = $product->is_type('variation') ? $product->get_parent_id() : $product->get_id();
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ class LicensedProduct extends WC_Product
|
|||||||
return $this->exists() && $this->get_price() !== '';
|
return $this->exists() && $this->get_price() !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed products are always in stock (virtual, no inventory)
|
||||||
|
*/
|
||||||
|
public function is_in_stock(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get max activations for this product
|
* Get max activations for this product
|
||||||
* Falls back to default settings if not set on product
|
* Falls back to default settings if not set on product
|
||||||
|
|||||||
@@ -30,9 +30,12 @@ final class LicensedProductType
|
|||||||
*/
|
*/
|
||||||
private function registerHooks(): void
|
private function registerHooks(): void
|
||||||
{
|
{
|
||||||
|
// Ensure product type terms exist in taxonomy (for WC_Product_Factory::get_product_type())
|
||||||
|
add_action('woocommerce_init', [$this, 'ensureProductTypeTermsExist']);
|
||||||
|
|
||||||
// Register product types
|
// 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, 4);
|
||||||
|
|
||||||
// Add product data tabs
|
// Add product data tabs
|
||||||
add_filter('woocommerce_product_data_tabs', [$this, 'addProductDataTab']);
|
add_filter('woocommerce_product_data_tabs', [$this, 'addProductDataTab']);
|
||||||
@@ -46,9 +49,19 @@ final class LicensedProductType
|
|||||||
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']);
|
add_action('woocommerce_licensed-variable_add_to_cart', [$this, 'variableAddToCartTemplate']);
|
||||||
|
|
||||||
|
// Use variable product add-to-cart handler for licensed-variable products
|
||||||
|
add_filter('woocommerce_add_to_cart_handler', [$this, 'addToCartHandler'], 10, 2);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
|
// Hide stock HTML for licensed products
|
||||||
|
add_filter('woocommerce_get_stock_html', [$this, 'hideStockHtml'], 10, 2);
|
||||||
|
add_filter('woocommerce_get_availability', [$this, 'hideAvailability'], 10, 2);
|
||||||
|
add_filter('woocommerce_get_availability_text', [$this, 'hideAvailabilityText'], 10, 2);
|
||||||
|
add_filter('woocommerce_product_get_stock_quantity', [$this, 'hideStockQuantity'], 10, 2);
|
||||||
|
add_filter('woocommerce_product_variation_get_stock_quantity', [$this, 'hideStockQuantity'], 10, 2);
|
||||||
|
|
||||||
// Display current version under product title on single product page
|
// Display current version under product title on single product page
|
||||||
add_action('woocommerce_single_product_summary', [$this, 'displayCurrentVersion'], 6);
|
add_action('woocommerce_single_product_summary', [$this, 'displayCurrentVersion'], 6);
|
||||||
|
|
||||||
@@ -64,6 +77,15 @@ final class LicensedProductType
|
|||||||
add_action('admin_footer', [$this, 'addVariableProductScripts']);
|
add_action('admin_footer', [$this, 'addVariableProductScripts']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure product type terms exist in the product_type taxonomy
|
||||||
|
* This is required for WC_Product_Factory::get_product_type() to work correctly
|
||||||
|
*/
|
||||||
|
public function ensureProductTypeTermsExist(): void
|
||||||
|
{
|
||||||
|
\Jeremias\WcLicensedProduct\Installer::registerProductTypes();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add product types to selector
|
* Add product types to selector
|
||||||
*/
|
*/
|
||||||
@@ -76,8 +98,13 @@ final class LicensedProductType
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get product class for licensed types
|
* Get product class for licensed types
|
||||||
|
*
|
||||||
|
* @param string $className Default class name
|
||||||
|
* @param string $productType Product type
|
||||||
|
* @param string $postType Post type (usually 'product' or 'product_variation')
|
||||||
|
* @param mixed $productId Product ID (can be int or string)
|
||||||
*/
|
*/
|
||||||
public function getProductClass(string $className, string $productType): string
|
public function getProductClass(string $className, string $productType, string $postType = '', $productId = 0): string
|
||||||
{
|
{
|
||||||
if ($productType === 'licensed') {
|
if ($productType === 'licensed') {
|
||||||
return LicensedProduct::class;
|
return LicensedProduct::class;
|
||||||
@@ -86,11 +113,24 @@ final class LicensedProductType
|
|||||||
return LicensedVariableProduct::class;
|
return LicensedVariableProduct::class;
|
||||||
}
|
}
|
||||||
// Handle variations of licensed-variable products
|
// Handle variations of licensed-variable products
|
||||||
if ($productType === 'variation') {
|
// Check both by product type and by post type for variations
|
||||||
// Check if parent is licensed-variable
|
if ($productType === 'variation' || $postType === 'product_variation') {
|
||||||
global $post;
|
// Get parent ID from the product post
|
||||||
if ($post && $post->post_parent) {
|
$parentId = 0;
|
||||||
$parentType = \WC_Product_Factory::get_product_type($post->post_parent);
|
$productIdInt = (int) $productId;
|
||||||
|
if ($productIdInt > 0) {
|
||||||
|
$parentId = wp_get_post_parent_id($productIdInt);
|
||||||
|
}
|
||||||
|
// Fallback to global $post if product ID not available
|
||||||
|
if (!$parentId) {
|
||||||
|
global $post;
|
||||||
|
if ($post && $post->post_parent) {
|
||||||
|
$parentId = (int) $post->post_parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parentId) {
|
||||||
|
$parentType = \WC_Product_Factory::get_product_type($parentId);
|
||||||
if ($parentType === 'licensed-variable') {
|
if ($parentType === 'licensed-variable') {
|
||||||
return LicensedProductVariation::class;
|
return LicensedProductVariation::class;
|
||||||
}
|
}
|
||||||
@@ -101,15 +141,23 @@ final class LicensedProductType
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add product data tab for license settings
|
* Add product data tab for license settings
|
||||||
|
* Also modify variations tab to show for licensed-variable products
|
||||||
*/
|
*/
|
||||||
public function addProductDataTab(array $tabs): array
|
public function addProductDataTab(array $tabs): array
|
||||||
{
|
{
|
||||||
|
// Add our License Settings tab
|
||||||
$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', 'show_if_licensed-variable'],
|
'class' => ['show_if_licensed', 'show_if_licensed-variable'],
|
||||||
'priority' => 21,
|
'priority' => 21,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Make Variations tab also show for licensed-variable products
|
||||||
|
if (isset($tabs['variations'])) {
|
||||||
|
$tabs['variations']['class'][] = 'show_if_licensed-variable';
|
||||||
|
}
|
||||||
|
|
||||||
return $tabs;
|
return $tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,35 +247,6 @@ final class LicensedProductType
|
|||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
|
||||||
jQuery(document).ready(function($) {
|
|
||||||
// Show/hide panels based on product type for license settings tab
|
|
||||||
function toggleLicensedProductOptions() {
|
|
||||||
var productType = $('#product-type').val();
|
|
||||||
var isLicensed = productType === 'licensed';
|
|
||||||
var isLicensedVariable = productType === 'licensed-variable';
|
|
||||||
|
|
||||||
if (isLicensed || isLicensedVariable) {
|
|
||||||
// Show license settings tab
|
|
||||||
$('.show_if_licensed').show();
|
|
||||||
$('.show_if_licensed-variable').show();
|
|
||||||
$('.general_options').show();
|
|
||||||
$('.pricing').show();
|
|
||||||
$('.general_tab').show();
|
|
||||||
} else {
|
|
||||||
// Hide license settings tab for other product types
|
|
||||||
$('.show_if_licensed').hide();
|
|
||||||
$('.show_if_licensed-variable').hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial state on page load
|
|
||||||
toggleLicensedProductOptions();
|
|
||||||
|
|
||||||
// On product type change
|
|
||||||
$('#product-type').on('change', toggleLicensedProductOptions);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,26 +284,111 @@ final class LicensedProductType
|
|||||||
wc_get_template('single-product/add-to-cart/simple.php');
|
wc_get_template('single-product/add-to-cart/simple.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the variable product add-to-cart handler for licensed-variable products
|
||||||
|
* WooCommerce uses product type to determine which handler to use
|
||||||
|
*/
|
||||||
|
public function addToCartHandler(string $handler, \WC_Product $product): string
|
||||||
|
{
|
||||||
|
if ($product->is_type('licensed-variable')) {
|
||||||
|
return 'variable';
|
||||||
|
}
|
||||||
|
return $handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide stock HTML for licensed products (they're always virtual/in-stock)
|
||||||
|
*/
|
||||||
|
public function hideStockHtml(string $html, \WC_Product $product): string
|
||||||
|
{
|
||||||
|
if ($this->isLicensedProductOrVariation($product)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide availability data for licensed products (they're always virtual/in-stock)
|
||||||
|
*/
|
||||||
|
public function hideAvailability(array $availability, \WC_Product $product): array
|
||||||
|
{
|
||||||
|
if ($this->isLicensedProductOrVariation($product)) {
|
||||||
|
return [
|
||||||
|
'availability' => '',
|
||||||
|
'class' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $availability;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide availability text for licensed products
|
||||||
|
*/
|
||||||
|
public function hideAvailabilityText(string $availability, \WC_Product $product): string
|
||||||
|
{
|
||||||
|
if ($this->isLicensedProductOrVariation($product)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $availability;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide stock quantity for licensed products (return null = no stock display)
|
||||||
|
*
|
||||||
|
* @param int|null $quantity
|
||||||
|
* @param \WC_Product $product
|
||||||
|
* @return int|null
|
||||||
|
*/
|
||||||
|
public function hideStockQuantity($quantity, \WC_Product $product)
|
||||||
|
{
|
||||||
|
if ($this->isLicensedProductOrVariation($product)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product is a licensed product or variation of one
|
||||||
|
*/
|
||||||
|
private function isLicensedProductOrVariation(\WC_Product $product): bool
|
||||||
|
{
|
||||||
|
// Direct licensed products
|
||||||
|
if ($product->is_type('licensed') || $product->is_type('licensed-variable')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check by class name for our custom variation class
|
||||||
|
if ($product instanceof LicensedProductVariation) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a variation with a licensed-variable parent
|
||||||
|
// Use WC_Product_Factory::get_product_type() to get parent type directly from DB
|
||||||
|
// This is more reliable than loading the full product object
|
||||||
|
$parentId = $product->get_parent_id();
|
||||||
|
if ($parentId) {
|
||||||
|
$parentType = \WC_Product_Factory::get_product_type($parentId);
|
||||||
|
if ($parentType === 'licensed-variable') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make licensed products virtual by default
|
* Make licensed products virtual by default
|
||||||
*/
|
*/
|
||||||
public function isVirtual(bool $isVirtual, \WC_Product $product): bool
|
public function isVirtual(bool $isVirtual, \WC_Product $product): bool
|
||||||
{
|
{
|
||||||
if ($product->is_type('licensed') || $product->is_type('licensed-variable')) {
|
if ($this->isLicensedProductOrVariation($product)) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue frontend styles for licensed products on single product pages
|
* Enqueue frontend styles and scripts for licensed products on single product pages
|
||||||
*/
|
*/
|
||||||
public function enqueueFrontendStyles(): void
|
public function enqueueFrontendStyles(): void
|
||||||
{
|
{
|
||||||
@@ -304,6 +408,11 @@ final class LicensedProductType
|
|||||||
[],
|
[],
|
||||||
WC_LICENSED_PRODUCT_VERSION
|
WC_LICENSED_PRODUCT_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// For licensed-variable products, enqueue WooCommerce variation scripts
|
||||||
|
if ($product->is_type('licensed-variable')) {
|
||||||
|
wp_enqueue_script('wc-add-to-cart-variation');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -333,24 +442,56 @@ final class LicensedProductType
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add to cart template for variable licensed products
|
* Add to cart template for variable licensed products
|
||||||
|
* This mirrors WooCommerce's woocommerce_variable_add_to_cart() function
|
||||||
*/
|
*/
|
||||||
public function variableAddToCartTemplate(): void
|
public function variableAddToCartTemplate(): void
|
||||||
{
|
{
|
||||||
global $product;
|
global $product;
|
||||||
|
|
||||||
if (!$product instanceof \WC_Product_Variable) {
|
// The hook woocommerce_licensed-variable_add_to_cart only fires for this product type
|
||||||
|
// so we just need to verify the product exists
|
||||||
|
if (!$product) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure we're working with a product that has variable product methods
|
||||||
|
// Re-load the product to ensure we get the correct class instance
|
||||||
|
$productId = $product->get_id();
|
||||||
|
$variableProduct = wc_get_product($productId);
|
||||||
|
|
||||||
|
if (!$variableProduct || !method_exists($variableProduct, 'get_variation_attributes')) {
|
||||||
|
// Fallback to simple add to cart if not a variable product
|
||||||
|
wc_get_template('single-product/add-to-cart/simple.php');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global $product to use the correctly loaded instance
|
||||||
|
// This ensures the template has the right product type
|
||||||
|
$product = $variableProduct;
|
||||||
|
|
||||||
// Get variations count to determine if we should load them via AJAX
|
// Get variations count to determine if we should load them via AJAX
|
||||||
$getVariations = count($product->get_children()) <= apply_filters('woocommerce_ajax_variation_threshold', 30, $product);
|
$children = $variableProduct->get_children();
|
||||||
|
$getVariations = count($children) <= apply_filters('woocommerce_ajax_variation_threshold', 30, $variableProduct);
|
||||||
|
|
||||||
|
// Get template variables - WooCommerce expects these to be set
|
||||||
|
$availableVariations = $getVariations ? $variableProduct->get_available_variations() : false;
|
||||||
|
$attributes = $variableProduct->get_variation_attributes();
|
||||||
|
$selectedAttributes = $variableProduct->get_default_attributes();
|
||||||
|
|
||||||
|
// Ensure arrays (WooCommerce template expects arrays, not null)
|
||||||
|
if (!is_array($attributes)) {
|
||||||
|
$attributes = [];
|
||||||
|
}
|
||||||
|
if (!is_array($selectedAttributes)) {
|
||||||
|
$selectedAttributes = [];
|
||||||
|
}
|
||||||
|
|
||||||
wc_get_template(
|
wc_get_template(
|
||||||
'single-product/add-to-cart/variable.php',
|
'single-product/add-to-cart/variable.php',
|
||||||
[
|
[
|
||||||
'available_variations' => $getVariations ? $product->get_available_variations() : false,
|
'available_variations' => $availableVariations,
|
||||||
'attributes' => $product->get_variation_attributes(),
|
'attributes' => $attributes,
|
||||||
'selected_attributes' => $product->get_default_attributes(),
|
'selected_attributes' => $selectedAttributes,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -492,7 +633,8 @@ final class LicensedProductType
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add JavaScript for licensed-variable product type in admin
|
* Add JavaScript for licensed product types in admin
|
||||||
|
* Handles visibility of License Settings tab and Product Versions meta box
|
||||||
*/
|
*/
|
||||||
public function addVariableProductScripts(): void
|
public function addVariableProductScripts(): void
|
||||||
{
|
{
|
||||||
@@ -509,60 +651,63 @@ final class LicensedProductType
|
|||||||
?>
|
?>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
jQuery(document).ready(function($) {
|
jQuery(document).ready(function($) {
|
||||||
// Show/hide panels based on product type
|
// Handle our custom License Settings tab, Product Versions meta box,
|
||||||
function toggleLicensedVariableOptions() {
|
// and show_if_licensed-variable elements
|
||||||
|
function toggleOurElements() {
|
||||||
var productType = $('#product-type').val();
|
var productType = $('#product-type').val();
|
||||||
|
var isLicensed = productType === 'licensed';
|
||||||
|
var isLicensedVariable = productType === 'licensed-variable';
|
||||||
|
|
||||||
if (productType === 'licensed-variable') {
|
// License Settings tab - use CSS class for visibility
|
||||||
// Show variable product options
|
var $licenseTab = $('li.licensed_product_options');
|
||||||
|
if (isLicensed || isLicensedVariable) {
|
||||||
|
$licenseTab.addClass('wclp-active');
|
||||||
|
} else {
|
||||||
|
$licenseTab.removeClass('wclp-active');
|
||||||
|
// If License Settings panel is active, switch to General tab
|
||||||
|
if ($('#licensed_product_data').is(':visible')) {
|
||||||
|
$('li.general_options a').trigger('click');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product Versions meta box
|
||||||
|
var $metaBox = $('#wc_licensed_product_versions');
|
||||||
|
if (isLicensed || isLicensedVariable) {
|
||||||
|
$metaBox.css('display', '');
|
||||||
|
} else {
|
||||||
|
$metaBox.css('display', 'none');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle show_if_licensed-variable elements (like Variations tab)
|
||||||
|
// WooCommerce doesn't know about our custom product types
|
||||||
|
if (isLicensedVariable) {
|
||||||
|
$('.show_if_licensed-variable').show();
|
||||||
|
// Also show elements that should be visible for variable products
|
||||||
|
// since licensed-variable is a variable product type
|
||||||
$('.show_if_variable').show();
|
$('.show_if_variable').show();
|
||||||
$('.hide_if_variable').hide();
|
$('.hide_if_variable').hide();
|
||||||
|
} else {
|
||||||
// Show licensed product options
|
// Let WooCommerce handle show_if_variable elements
|
||||||
$('.show_if_licensed-variable').show();
|
// We only need to hide our custom class when not licensed-variable
|
||||||
$('.show_if_licensed').show();
|
// Don't hide show_if_licensed-variable when it's licensed (simple)
|
||||||
|
if (!isLicensed) {
|
||||||
// Show general and variations tabs
|
$('.show_if_licensed-variable').not('.show_if_licensed').hide();
|
||||||
$('.general_tab').show();
|
}
|
||||||
$('.variations_tab').show();
|
|
||||||
$('.variations_options').show();
|
|
||||||
|
|
||||||
// Hide shipping tab (virtual products)
|
|
||||||
$('.shipping_tab').hide();
|
|
||||||
|
|
||||||
// Ensure the variations panel can be displayed
|
|
||||||
$('#variable_product_options').show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial check
|
// Initial setup - run after WooCommerce has initialized
|
||||||
toggleLicensedVariableOptions();
|
setTimeout(toggleOurElements, 10);
|
||||||
|
|
||||||
// On product type change
|
// On product type change - run after WooCommerce has processed
|
||||||
$('#product-type').on('change', function() {
|
$('#product-type').on('change', function() {
|
||||||
// Use setTimeout to let WooCommerce finish its own processing first
|
setTimeout(toggleOurElements, 100);
|
||||||
setTimeout(toggleLicensedVariableOptions, 100);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-apply after WooCommerce AJAX operations that may reset visibility
|
// Re-apply after WooCommerce AJAX operations
|
||||||
$(document).on('woocommerce_variations_loaded', toggleLicensedVariableOptions);
|
$(document).on('woocommerce_variations_loaded woocommerce_variations_added woocommerce_variations_saved', function() {
|
||||||
$(document).on('woocommerce_variations_added', toggleLicensedVariableOptions);
|
setTimeout(toggleOurElements, 10);
|
||||||
$(document).on('woocommerce_variations_saved', toggleLicensedVariableOptions);
|
|
||||||
|
|
||||||
// Handle AJAX complete events for attribute saving
|
|
||||||
$(document).ajaxComplete(function(event, xhr, settings) {
|
|
||||||
// Check if this was a product data save or attribute action
|
|
||||||
if (settings.data && (
|
|
||||||
settings.data.indexOf('action=woocommerce_save_attributes') !== -1 ||
|
|
||||||
settings.data.indexOf('action=woocommerce_load_variations') !== -1 ||
|
|
||||||
settings.data.indexOf('action=woocommerce_add_variation') !== -1
|
|
||||||
)) {
|
|
||||||
setTimeout(toggleLicensedVariableOptions, 100);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also listen for the WooCommerce product type show/hide trigger
|
|
||||||
$('body').on('woocommerce-product-type-change', toggleLicensedVariableOptions);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -35,6 +35,61 @@ class LicensedProductVariation extends WC_Product_Variation
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed products are always in stock (virtual, no inventory)
|
||||||
|
*/
|
||||||
|
public function is_in_stock(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get availability - empty for licensed products (no stock indicator)
|
||||||
|
*/
|
||||||
|
public function get_availability(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'availability' => '',
|
||||||
|
'class' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't manage stock for licensed products
|
||||||
|
*/
|
||||||
|
public function managing_stock(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if variation is purchasable
|
||||||
|
* Override to handle custom parent product type
|
||||||
|
*/
|
||||||
|
public function is_purchasable(): bool
|
||||||
|
{
|
||||||
|
// Check if variation exists
|
||||||
|
if (!$this->exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check parent product status
|
||||||
|
$parentId = $this->get_parent_id();
|
||||||
|
$parentStatus = get_post_status($parentId);
|
||||||
|
|
||||||
|
if ($parentStatus !== 'publish' && !current_user_can('edit_post', $parentId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if variation has a price
|
||||||
|
$price = $this->get_price();
|
||||||
|
if ($price === '' || $price === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply_filters('woocommerce_variation_is_purchasable', true, $this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get max activations for this variation
|
* Get max activations for this variation
|
||||||
* Falls back to parent product, then to default settings
|
* Falls back to parent product, then to default settings
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ class LicensedVariableProduct extends WC_Product_Variable
|
|||||||
return 'licensed-variable';
|
return 'licensed-variable';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product is of a certain type
|
||||||
|
* Override to return true for 'variable' as well, so WooCommerce internal
|
||||||
|
* checks pass (many methods in WC_Product_Variable check is_type('variable'))
|
||||||
|
*/
|
||||||
|
public function is_type($type): bool
|
||||||
|
{
|
||||||
|
if (is_array($type)) {
|
||||||
|
return in_array($this->get_type(), $type, true) || in_array('variable', $type, true);
|
||||||
|
}
|
||||||
|
return $this->get_type() === $type || 'variable' === $type;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Licensed products are always virtual
|
* Licensed products are always virtual
|
||||||
*/
|
*/
|
||||||
@@ -50,11 +63,197 @@ class LicensedVariableProduct extends WC_Product_Variable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Licensed products are purchasable
|
* Licensed variable products are purchasable if the parent check passes
|
||||||
|
* Variable products don't have a direct price - their variations do
|
||||||
*/
|
*/
|
||||||
public function is_purchasable(): bool
|
public function is_purchasable(): bool
|
||||||
{
|
{
|
||||||
return $this->exists() && $this->get_price() !== '';
|
// Use the parent WC_Product_Variable logic
|
||||||
|
// which checks exists() and status, not price
|
||||||
|
return parent::is_purchasable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Licensed products are always in stock (virtual, no inventory)
|
||||||
|
*/
|
||||||
|
public function is_in_stock(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get children (variations) for this product
|
||||||
|
* Override because WC_Product_Variable::get_children() checks is_type('variable')
|
||||||
|
* which fails for our 'licensed-variable' type
|
||||||
|
*/
|
||||||
|
public function get_children($context = 'view'): array
|
||||||
|
{
|
||||||
|
if (!$this->get_id()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query variations directly from database since WooCommerce's data store
|
||||||
|
// doesn't work properly with custom variable product types
|
||||||
|
global $wpdb;
|
||||||
|
$children = $wpdb->get_col($wpdb->prepare(
|
||||||
|
"SELECT ID FROM {$wpdb->posts}
|
||||||
|
WHERE post_parent = %d
|
||||||
|
AND post_type = 'product_variation'
|
||||||
|
AND post_status IN ('publish', 'private')
|
||||||
|
ORDER BY menu_order ASC, ID ASC",
|
||||||
|
$this->get_id()
|
||||||
|
));
|
||||||
|
|
||||||
|
$children = array_map('intval', $children);
|
||||||
|
|
||||||
|
if ('view' === $context) {
|
||||||
|
$children = apply_filters('woocommerce_get_children', $children, $this, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($children) ? $children : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variation attributes for this product
|
||||||
|
* Override because WC_Product_Variable uses data_store which doesn't work
|
||||||
|
* properly with custom variable product types
|
||||||
|
*/
|
||||||
|
public function get_variation_attributes(): array
|
||||||
|
{
|
||||||
|
$attributes = $this->get_attributes();
|
||||||
|
|
||||||
|
if (!$attributes || !is_array($attributes)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$variation_attributes = [];
|
||||||
|
|
||||||
|
foreach ($attributes as $attribute) {
|
||||||
|
// For WC_Product_Attribute objects
|
||||||
|
if ($attribute instanceof \WC_Product_Attribute) {
|
||||||
|
if ($attribute->get_variation()) {
|
||||||
|
$attribute_name = $attribute->get_name();
|
||||||
|
|
||||||
|
// For taxonomy attributes, get term slugs
|
||||||
|
if ($attribute->is_taxonomy()) {
|
||||||
|
$attribute_terms = wc_get_product_terms(
|
||||||
|
$this->get_id(),
|
||||||
|
$attribute_name,
|
||||||
|
['fields' => 'slugs']
|
||||||
|
);
|
||||||
|
$variation_attributes[$attribute_name] = $attribute_terms;
|
||||||
|
} else {
|
||||||
|
// For custom attributes, get options directly
|
||||||
|
$variation_attributes[$attribute_name] = $attribute->get_options();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For array-based attributes (older format)
|
||||||
|
elseif (is_array($attribute) && !empty($attribute['is_variation'])) {
|
||||||
|
$attribute_name = $attribute['name'];
|
||||||
|
$values = isset($attribute['value']) ? explode('|', $attribute['value']) : [];
|
||||||
|
$variation_attributes[$attribute_name] = array_map('trim', $values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $variation_attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get variation prices (regular, sale, and final prices)
|
||||||
|
* Override because WC_Product_Variable uses data_store which doesn't work
|
||||||
|
* properly with custom variable product types
|
||||||
|
*/
|
||||||
|
public function get_variation_prices($for_display = false): array
|
||||||
|
{
|
||||||
|
$children = $this->get_children();
|
||||||
|
|
||||||
|
if (empty($children)) {
|
||||||
|
return [
|
||||||
|
'price' => [],
|
||||||
|
'regular_price' => [],
|
||||||
|
'sale_price' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$prices = [
|
||||||
|
'price' => [],
|
||||||
|
'regular_price' => [],
|
||||||
|
'sale_price' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($children as $child_id) {
|
||||||
|
$variation = wc_get_product($child_id);
|
||||||
|
if ($variation) {
|
||||||
|
$price = $variation->get_price();
|
||||||
|
$regular_price = $variation->get_regular_price();
|
||||||
|
$sale_price = $variation->get_sale_price();
|
||||||
|
|
||||||
|
if ('' !== $price) {
|
||||||
|
$prices['price'][$child_id] = $price;
|
||||||
|
}
|
||||||
|
if ('' !== $regular_price) {
|
||||||
|
$prices['regular_price'][$child_id] = $regular_price;
|
||||||
|
}
|
||||||
|
if ('' !== $sale_price) {
|
||||||
|
$prices['sale_price'][$child_id] = $sale_price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort prices
|
||||||
|
asort($prices['price']);
|
||||||
|
asort($prices['regular_price']);
|
||||||
|
asort($prices['sale_price']);
|
||||||
|
|
||||||
|
$this->prices_array = $prices;
|
||||||
|
|
||||||
|
return $this->prices_array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available variations for this product
|
||||||
|
* Override because WC_Product_Variable uses data_store which doesn't work
|
||||||
|
* properly with custom variable product types
|
||||||
|
*/
|
||||||
|
public function get_available_variations($return = 'array')
|
||||||
|
{
|
||||||
|
$children = $this->get_children();
|
||||||
|
$available_variations = [];
|
||||||
|
|
||||||
|
foreach ($children as $child_id) {
|
||||||
|
$variation = wc_get_product($child_id);
|
||||||
|
|
||||||
|
if (!$variation) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if variation should be available
|
||||||
|
if (!$variation->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if purchasable (has price)
|
||||||
|
if (!$variation->is_purchasable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build variation data
|
||||||
|
if ($return === 'array') {
|
||||||
|
$variationData = $this->get_available_variation($variation);
|
||||||
|
// Override availability_html to be empty for licensed products
|
||||||
|
$variationData['availability_html'] = '';
|
||||||
|
$available_variations[] = $variationData;
|
||||||
|
} else {
|
||||||
|
$available_variations[] = $variation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($return === 'array') {
|
||||||
|
$available_variations = array_values(array_filter($available_variations));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $available_variations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
417
src/Update/PluginUpdateChecker.php
Normal file
417
src/Update/PluginUpdateChecker.php
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Update Checker
|
||||||
|
*
|
||||||
|
* Checks for plugin updates from the configured license server.
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Update
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Update;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles checking for plugin updates from the license server
|
||||||
|
*
|
||||||
|
* This class hooks into WordPress's native plugin update system to check for
|
||||||
|
* updates from the configured license server. It validates the license and
|
||||||
|
* provides download authentication.
|
||||||
|
*/
|
||||||
|
final class PluginUpdateChecker
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cache key for update info
|
||||||
|
*/
|
||||||
|
private const CACHE_KEY = 'wclp_update_info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default cache TTL (12 hours)
|
||||||
|
*/
|
||||||
|
private const DEFAULT_CACHE_TTL = 43200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton instance
|
||||||
|
*/
|
||||||
|
private static ?self $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin slug
|
||||||
|
*/
|
||||||
|
private string $pluginSlug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin basename (slug/slug.php)
|
||||||
|
*/
|
||||||
|
private string $pluginBasename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance
|
||||||
|
*/
|
||||||
|
public static function getInstance(): self
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor for singleton
|
||||||
|
*/
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$this->pluginSlug = 'wc-licensed-product';
|
||||||
|
$this->pluginBasename = WC_LICENSED_PRODUCT_PLUGIN_BASENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register WordPress hooks for update checking
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Skip if auto-updates are disabled
|
||||||
|
if ($this->isAutoUpdateDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
add_filter('pre_set_site_transient_update_plugins', [$this, 'checkForUpdates']);
|
||||||
|
|
||||||
|
// Provide plugin information for the update modal
|
||||||
|
add_filter('plugins_api', [$this, 'getPluginInfo'], 10, 3);
|
||||||
|
|
||||||
|
// Add authentication headers to download requests
|
||||||
|
add_filter('http_request_args', [$this, 'addAuthHeaders'], 10, 2);
|
||||||
|
|
||||||
|
// Clear cache on settings save
|
||||||
|
add_action('update_option_wc_licensed_product_plugin_license_key', [$this, 'clearCache']);
|
||||||
|
add_action('update_option_wc_licensed_product_plugin_license_server_url', [$this, 'clearCache']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if auto-updates are disabled
|
||||||
|
*/
|
||||||
|
private function isAutoUpdateDisabled(): bool
|
||||||
|
{
|
||||||
|
// Check constant
|
||||||
|
if (defined('WC_LICENSE_DISABLE_AUTO_UPDATE') && WC_LICENSE_DISABLE_AUTO_UPDATE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check setting
|
||||||
|
$enabled = get_option('wc_licensed_product_plugin_auto_update_enabled', 'yes');
|
||||||
|
return $enabled !== 'yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for plugin updates
|
||||||
|
*
|
||||||
|
* @param object $transient The update_plugins transient
|
||||||
|
* @return object Modified transient
|
||||||
|
*/
|
||||||
|
public function checkForUpdates($transient)
|
||||||
|
{
|
||||||
|
if (empty($transient->checked)) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cached update info or fetch fresh
|
||||||
|
$updateInfo = $this->getUpdateInfo();
|
||||||
|
|
||||||
|
if (!$updateInfo || !isset($updateInfo['update_available']) || !$updateInfo['update_available']) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
$currentVersion = $transient->checked[$this->pluginBasename] ?? WC_LICENSED_PRODUCT_VERSION;
|
||||||
|
|
||||||
|
if (version_compare($updateInfo['version'], $currentVersion, '>')) {
|
||||||
|
$transient->response[$this->pluginBasename] = $this->buildUpdateObject($updateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin information for the update modal
|
||||||
|
*
|
||||||
|
* @param false|object|array $result The result object or array
|
||||||
|
* @param string $action The API action
|
||||||
|
* @param object $args Request arguments
|
||||||
|
* @return false|object
|
||||||
|
*/
|
||||||
|
public function getPluginInfo($result, string $action, object $args)
|
||||||
|
{
|
||||||
|
if ($action !== 'plugin_information') {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($args->slug) || $args->slug !== $this->pluginSlug) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get update info
|
||||||
|
$updateInfo = $this->getUpdateInfo(true);
|
||||||
|
|
||||||
|
if (!$updateInfo) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildPluginInfoObject($updateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add authentication headers to download requests
|
||||||
|
*
|
||||||
|
* @param array $args HTTP request arguments
|
||||||
|
* @param string $url Request URL
|
||||||
|
* @return array Modified arguments
|
||||||
|
*/
|
||||||
|
public function addAuthHeaders(array $args, string $url): array
|
||||||
|
{
|
||||||
|
// Only modify requests to our license server
|
||||||
|
$serverUrl = $this->getLicenseServerUrl();
|
||||||
|
if (empty($serverUrl) || strpos($url, parse_url($serverUrl, PHP_URL_HOST)) === false) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only modify download requests
|
||||||
|
if (strpos($url, 'license-download') === false) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add license key to headers for potential server-side verification
|
||||||
|
$licenseKey = $this->getLicenseKey();
|
||||||
|
if (!empty($licenseKey)) {
|
||||||
|
$args['headers']['X-License-Key'] = $licenseKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get update info from cache or server
|
||||||
|
*
|
||||||
|
* @param bool $forceRefresh Force refresh from server
|
||||||
|
* @return array|null Update info or null if unavailable
|
||||||
|
*/
|
||||||
|
public function getUpdateInfo(bool $forceRefresh = false): ?array
|
||||||
|
{
|
||||||
|
// Check cache unless force refresh
|
||||||
|
if (!$forceRefresh) {
|
||||||
|
$cached = get_transient(self::CACHE_KEY);
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from server
|
||||||
|
$updateInfo = $this->fetchUpdateInfo();
|
||||||
|
|
||||||
|
if ($updateInfo) {
|
||||||
|
// Cache the result
|
||||||
|
$cacheTtl = $this->getCacheTtl();
|
||||||
|
set_transient(self::CACHE_KEY, $updateInfo, $cacheTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updateInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch update info from the license server
|
||||||
|
*/
|
||||||
|
private function fetchUpdateInfo(): ?array
|
||||||
|
{
|
||||||
|
$serverUrl = $this->getLicenseServerUrl();
|
||||||
|
$licenseKey = $this->getLicenseKey();
|
||||||
|
|
||||||
|
if (empty($serverUrl) || empty($licenseKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$httpClient = HttpClient::create([
|
||||||
|
'timeout' => 15,
|
||||||
|
'verify_peer' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updateCheckUrl = rtrim($serverUrl, '/') . '/wp-json/wc-licensed-product/v1/update-check';
|
||||||
|
|
||||||
|
$response = $httpClient->request('POST', $updateCheckUrl, [
|
||||||
|
'json' => [
|
||||||
|
'license_key' => $licenseKey,
|
||||||
|
'domain' => $this->getCurrentDomain(),
|
||||||
|
'plugin_slug' => $this->pluginSlug,
|
||||||
|
'current_version' => WC_LICENSED_PRODUCT_VERSION,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->toArray();
|
||||||
|
|
||||||
|
// Verify response structure
|
||||||
|
if (!isset($data['success']) || !$data['success']) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Log error but don't break the site
|
||||||
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
|
error_log('WC Licensed Product: Update check failed - ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build WordPress update object for transient
|
||||||
|
*/
|
||||||
|
private function buildUpdateObject(array $updateInfo): object
|
||||||
|
{
|
||||||
|
$update = new \stdClass();
|
||||||
|
$update->id = $this->pluginSlug;
|
||||||
|
$update->slug = $updateInfo['slug'] ?? $this->pluginSlug;
|
||||||
|
$update->plugin = $this->pluginBasename;
|
||||||
|
$update->new_version = $updateInfo['version'];
|
||||||
|
$update->url = $updateInfo['homepage'] ?? '';
|
||||||
|
$update->package = $updateInfo['download_url'] ?? $updateInfo['package'] ?? '';
|
||||||
|
|
||||||
|
if (isset($updateInfo['tested'])) {
|
||||||
|
$update->tested = $updateInfo['tested'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($updateInfo['requires'])) {
|
||||||
|
$update->requires = $updateInfo['requires'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($updateInfo['requires_php'])) {
|
||||||
|
$update->requires_php = $updateInfo['requires_php'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($updateInfo['icons'])) {
|
||||||
|
$update->icons = $updateInfo['icons'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $update;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build plugin info object for plugins_api
|
||||||
|
*/
|
||||||
|
private function buildPluginInfoObject(array $updateInfo): object
|
||||||
|
{
|
||||||
|
$info = new \stdClass();
|
||||||
|
$info->name = $updateInfo['name'] ?? 'WC Licensed Product';
|
||||||
|
$info->slug = $updateInfo['slug'] ?? $this->pluginSlug;
|
||||||
|
$info->version = $updateInfo['version'];
|
||||||
|
$info->author = '<a href="https://src.bundespruefstelle.ch/magdev">Marco Graetsch</a>';
|
||||||
|
$info->homepage = $updateInfo['homepage'] ?? '';
|
||||||
|
$info->requires = $updateInfo['requires'] ?? '6.0';
|
||||||
|
$info->tested = $updateInfo['tested'] ?? '';
|
||||||
|
$info->requires_php = $updateInfo['requires_php'] ?? '8.3';
|
||||||
|
$info->downloaded = 0;
|
||||||
|
$info->last_updated = $updateInfo['last_updated'] ?? '';
|
||||||
|
$info->download_link = $updateInfo['download_url'] ?? $updateInfo['package'] ?? '';
|
||||||
|
|
||||||
|
// Sections for the modal
|
||||||
|
$info->sections = [];
|
||||||
|
|
||||||
|
if (isset($updateInfo['sections']['description'])) {
|
||||||
|
$info->sections['description'] = $updateInfo['sections']['description'];
|
||||||
|
} else {
|
||||||
|
$info->sections['description'] = __(
|
||||||
|
'WooCommerce plugin for selling licensed software products with domain-bound license keys.',
|
||||||
|
'wc-licensed-product'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($updateInfo['sections']['changelog']) || isset($updateInfo['changelog'])) {
|
||||||
|
$info->sections['changelog'] = $updateInfo['sections']['changelog'] ?? $updateInfo['changelog'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Banners and icons
|
||||||
|
if (isset($updateInfo['banners'])) {
|
||||||
|
$info->banners = $updateInfo['banners'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($updateInfo['icons'])) {
|
||||||
|
$info->icons = $updateInfo['icons'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the update cache
|
||||||
|
*/
|
||||||
|
public function clearCache(): void
|
||||||
|
{
|
||||||
|
delete_transient(self::CACHE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache TTL from settings or default
|
||||||
|
*/
|
||||||
|
private function getCacheTtl(): int
|
||||||
|
{
|
||||||
|
$hours = (int) get_option('wc_licensed_product_update_check_frequency', 12);
|
||||||
|
return max(1, $hours) * HOUR_IN_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the license server URL from settings
|
||||||
|
*/
|
||||||
|
private function getLicenseServerUrl(): string
|
||||||
|
{
|
||||||
|
// Check constant override first
|
||||||
|
if (defined('WC_LICENSE_UPDATE_CHECK_URL') && WC_LICENSE_UPDATE_CHECK_URL) {
|
||||||
|
return WC_LICENSE_UPDATE_CHECK_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) get_option('wc_licensed_product_plugin_license_server_url', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the license key from settings
|
||||||
|
*/
|
||||||
|
private function getLicenseKey(): string
|
||||||
|
{
|
||||||
|
return (string) get_option('wc_licensed_product_plugin_license_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current domain from the site URL
|
||||||
|
*/
|
||||||
|
private function getCurrentDomain(): string
|
||||||
|
{
|
||||||
|
$siteUrl = get_site_url();
|
||||||
|
$parsed = parse_url($siteUrl);
|
||||||
|
$host = $parsed['host'] ?? 'localhost';
|
||||||
|
|
||||||
|
if (isset($parsed['port'])) {
|
||||||
|
$host .= ':' . $parsed['port'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($host);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force an immediate update check
|
||||||
|
*
|
||||||
|
* Useful for admin interfaces where user clicks "Check for updates"
|
||||||
|
*/
|
||||||
|
public function forceUpdateCheck(): ?array
|
||||||
|
{
|
||||||
|
$this->clearCache();
|
||||||
|
return $this->getUpdateInfo(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.8
|
* Version: 0.6.0
|
||||||
* 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.8');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.6.0');
|
||||||
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