23 Commits

Author SHA1 Message Date
b670bacf27 Add WordPress auto-update functionality (v0.6.0)
- Add UpdateController REST API endpoint for serving update info to licensed plugins
- Add PluginUpdateChecker singleton for client-side update checking
- Hook into WordPress native plugin update system (pre_set_site_transient_update_plugins, plugins_api)
- Add Auto-Updates settings subtab with enable/disable and check frequency options
- Add authentication headers for secure download requests
- Support configurable cache TTL for update checks (default 12 hours)
- Document /update-check endpoint in OpenAPI specification
- Update German translations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 20:14:11 +01:00
f8f6434342 Update CLAUDE.md with v0.5.14 and v0.5.15 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:48:36 +01:00
dace416608 Add checksum file for v0.5.15 release
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:43:36 +01:00
72017f4c62 Fix tab rendering bug in WooCommerce product edit page (v0.5.15)
- Simplified JavaScript to avoid conflicts with WooCommerce's native show/hide logic
- Removed conflicting CSS rule for .hide_if_licensed
- License Settings tab uses CSS class toggle for proper display
- Variations tab properly shows for licensed-variable via woocommerce_product_data_tabs filter

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:39:55 +01:00
f9efe698ea Fix Product Versions meta box not appearing for licensed-variable products (v0.5.14)
- 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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:00:34 +01:00
d2e3b41a00 Add checksum file for v0.5.13 release
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 15:47:00 +01:00
4b6fafe500 Update CLAUDE.md with v0.5.12 and v0.5.13 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 15:46:12 +01:00
d29697ac62 Fix licenses not showing in admin order form for variable products (v0.5.13)
- Fix OrderLicenseController to use 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
- Remove debug logging from all source files (PHP and JavaScript)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 15:45:32 +01:00
142500cab0 Fix stock indicator on licensed variable products (v0.5.12)
- Fixed stock indicator appearing in cart for licensed variable products
- Override get_children() with direct SQL query to bypass WooCommerce type check
- Override get_variation_attributes() for proper taxonomy attribute loading
- Override get_variation_prices() to prevent null array errors
- Override get_available_variations() with empty availability_html
- Added is_type() override to pass variable type checks
- Added multiple stock-related filters for comprehensive coverage
- Improved isLicensedProductOrVariation() with DB-level parent type check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 14:44:57 +01:00
20fb39d1a1 Update CLAUDE.md with v0.5.8-0.5.11 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:59:00 +01:00
953aa6c8e8 Fix licensed variable products showing as sold out (v0.5.11)
- Fixed is_purchasable() method in LicensedVariableProduct to delegate to
  parent WC_Product_Variable instead of checking for price (variable products
  don't have direct prices, only their variations do)
- Fixed getProductClass() filter to accept all 4 WooCommerce parameters
  and use product_id for reliable variation parent detection
- Fallback to global $post when product_id not available for backwards compat

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:58:07 +01:00
db4966caf2 Add release package v0.5.10
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:52:51 +01:00
9c4232f14f Fix licensed variable products not showing variations (v0.5.10)
- Re-load product via wc_get_product() to ensure correct class instance
- Removed overly strict type check that prevented variations from displaying
- Now mirrors WooCommerce's standard woocommerce_variable_add_to_cart()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:51:46 +01:00
0638767ce3 Add release package v0.5.9
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:41:58 +01:00
9826c8181e Fix frontend error on licensed variable products without attributes (v0.5.9)
- Added null checks for get_variation_attributes(), get_available_variations(), get_default_attributes()
- Show informative message when product has no variations configured
- Changed product type check from instanceof to is_type() for better compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:40:50 +01:00
fa972ceaf0 Add release package v0.5.8
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:36:24 +01:00
3abf05cff3 Update translations for v0.5.8
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:35:02 +01:00
169eed65eb Fix critical error and variants tab on licensed variable products (v0.5.8)
- Fixed critical error on frontend product pages for licensed variable products
- Variable product add-to-cart template now passes required variables
- Variants tab no longer disappears when saving attributes
- Added WooCommerce AJAX event listeners for tab visibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 13:34:27 +01:00
90cb8d97bd Update CLAUDE.md with v0.5.6 and v0.5.7 session history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 11:59:48 +01:00
fc281f7f4a Add release package v0.5.7
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 11:56:26 +01:00
962368d35f Update translations for v0.5.7
- Updated POT template with 388 strings
- All German (de_CH) strings translated
- Recompiled .mo file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 11:55:18 +01:00
4dcace6f06 Remove Default prefix from settings labels (v0.5.7)
- Max Activations (was "Default Max Activations")
- License Validity (Days) (was "Default License Validity (Days)")
- Bind to Major Version (was "Default Bind to Major Version")

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 11:54:31 +01:00
62aecc0240 Add release package v0.5.6
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 11:38:54 +01:00
40 changed files with 2443 additions and 256 deletions

View File

@@ -7,6 +7,107 @@ 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
### Fixed
- **CRITICAL:** 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`)
- Variants tab no longer disappears when saving attributes on licensed variable products
- Added WooCommerce AJAX event listeners to maintain tab visibility during attribute operations
### Changed
- Improved JavaScript event handling for licensed-variable product type in admin
- Added listeners for `woocommerce_variations_loaded`, `woocommerce_variations_added`, `woocommerce_variations_saved` events
- Added AJAX complete handler for attribute save operations
## [0.5.7] - 2026-01-27
### Changed
- Removed "Default" prefix from setting labels on Default Settings page for cleaner UI
- Labels now read "Max Activations", "License Validity (Days)", and "Bind to Major Version"
## [0.5.6] - 2026-01-27 ## [0.5.6] - 2026-01-27
### Fixed ### Fixed

219
CLAUDE.md
View File

@@ -1437,3 +1437,222 @@ Critical bug fix for response signing. The key derivation algorithm was incompat
- Length: 32 bytes (256 bits) - Length: 32 bytes (256 bits)
- Info: license_key (context-specific info) - Info: license_key (context-specific info)
- **Breaking change for existing signatures** - customer secrets will change after upgrade - **Breaking change for existing signatures** - customer secrets will change after upgrade
### 2026-01-27 - Version 0.5.6 - License Settings Tab Visibility Fix
**Overview:**
Fixed License Settings tab visibility for non-licensed product types and updated README with v0.5.x features.
**Bug Fix:**
- License Settings tab now only shows for Licensed Product and Licensed Variable Product types
- Previously the tab was visible on all product types due to CSS `!important` override forcing `display: block`
**Modified files:**
- `assets/css/admin.css` - Changed from `display: block !important` to `display: none` for `.show_if_licensed` and `.show_if_licensed-variable`
- `src/Product/LicensedProductType.php` - Added consolidated `toggleLicensedProductOptions()` JavaScript function
- `README.md` - Updated with complete feature documentation for v0.5.x features
**Technical notes:**
- CSS now hides License Settings tab by default
- JavaScript `toggleLicensedProductOptions()` function shows/hides tab based on product type selector
- Function is called both on page load and on product type change
- README updated with: Variable Licensed Products, Multi-Domain Licensing, Per-License Secrets, Download Statistics, Configurable Rate Limiting
**Release v0.5.6:**
- Created release package: `releases/wc-licensed-product-0.5.6.zip` (1.1 MB)
- SHA256: `4d35a319fe4cb4e7055bae17fc030487ca05e5e9ac905f76d0ac62002bde4336`
- Tagged as `v0.5.6` and pushed to `main` branch
### 2026-01-27 - Version 0.5.7 - Settings UI Cleanup
**Overview:**
Removed redundant "Default" prefix from setting labels on the Default Settings page for cleaner UI.
**Changed:**
- "Max Activations" (was "Default Max Activations")
- "License Validity (Days)" (was "Default License Validity (Days)")
- "Bind to Major Version" (was "Default Bind to Major Version")
**Modified files:**
- `src/Admin/SettingsController.php` - Removed "Default" prefix from three setting labels
**Technical notes:**
- Labels are cleaner since the page section itself is already named "Default Settings"
- No functional changes, purely UI improvement
- Updated all translations (388 strings)
**Release v0.5.7:**
- Created release package: `releases/wc-licensed-product-0.5.7.zip` (856 KB)
- SHA256: `ceb4d57598f576f4f172153ff80df8c180ecd4dca873cf109327fc5ac718930f`
- 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

View File

@@ -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;

View File

@@ -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);
})(); })();

View File

@@ -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: \n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
"POT-Creation-Date: 2026-01-27 11:29+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"
@@ -310,10 +310,11 @@ msgstr "Speichern"
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613 #: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
#: src/Admin/DashboardWidgetController.php:136 #: src/Admin/DashboardWidgetController.php:136
#: src/Admin/OrderLicenseController.php:260 #: src/Admin/OrderLicenseController.php:260
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:136 #: src/Admin/SettingsController.php:192
#: src/Product/LicensedProductType.php:184 #: src/Product/LicensedProductVariation.php:194
#: src/Product/LicensedProductType.php:385 #: src/Product/LicensedProductType.php:164
#: src/Product/LicensedProductVariation.php:139 #: src/Product/LicensedProductType.php:212
#: src/Product/LicensedProductType.php:553
#: src/Frontend/AccountController.php:286 #: src/Frontend/AccountController.php:286
msgid "Lifetime" msgid "Lifetime"
msgstr "Lebenslang" msgstr "Lebenslang"
@@ -491,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."
@@ -498,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."
@@ -505,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."
@@ -512,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."
@@ -519,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."
@@ -540,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."
@@ -547,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."
@@ -554,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."
@@ -561,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."
@@ -1019,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."
@@ -1165,17 +1176,18 @@ 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/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:182
msgid "Default Max Activations" #: src/Product/LicensedProductType.php:570
msgstr "Standard Max. Aktivierungen" msgid "Max Activations"
msgstr "Max. Aktivierungen"
#: src/Admin/SettingsController.php:178 #: src/Admin/SettingsController.php:178
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/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:200
msgid "Default License Validity (Days)" msgid "License Validity (Days)"
msgstr "Standard Lizenz-Gültigkeit (Tage)" msgstr "Lizenz-Gültigkeit (Tage)"
#: src/Admin/SettingsController.php:189 #: src/Admin/SettingsController.php:189
msgid "" msgid ""
@@ -1185,9 +1197,9 @@ 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/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:218
msgid "Default Bind to Major Version" msgid "Bind to Major Version"
msgstr "Standard An Hauptversion binden" msgstr "An Hauptversion binden"
#: src/Admin/SettingsController.php:201 #: src/Admin/SettingsController.php:201
msgid "" msgid ""
@@ -1213,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 ""
@@ -1344,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
@@ -1355,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."
@@ -1363,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 ""
@@ -1436,62 +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/LicensedProductType.php:72 #: src/Product/LicensedProductVariation.php:198
msgid "Monthly"
msgstr "Monatlich"
#: src/Product/LicensedProductVariation.php:202
msgid "Quarterly"
msgstr "Vierteljährlich"
#: src/Product/LicensedProductVariation.php:206
msgid "Yearly"
msgstr "Jährlich"
#. translators: %d: number of days
#: src/Product/LicensedProductVariation.php:211
#, php-format
msgid "%d day"
msgid_plural "%d days"
msgstr[0] "%d Tag"
msgstr[1] "%d Tage"
#: 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:384 #: 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:154 #. translators: %d: default max activations value
#: src/Product/LicensedProductType.php:402 #: src/Product/LicensedProductType.php:185
msgid "Max Activations"
msgstr "Max. Aktivierungen"
#: src/Product/LicensedProductType.php:157
#, php-format #, php-format
msgid "Maximum number of domain activations per license. Default: %d" msgid "Maximum number of domain activations per license. Default: %d"
msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d" msgstr "Maximale Anzahl der Domain-Aktivierungen pro Lizenz. Standard: %d"
#: src/Product/LicensedProductType.php:172 #. translators: %s: default validity value
msgid "License Validity (Days)" #: src/Product/LicensedProductType.php:203
msgstr "Lizenz-Gültigkeit (Tage)"
#: src/Product/LicensedProductType.php:175
#, php-format #, php-format
msgid "Number of days the license is valid. Leave empty for default (%s)." msgid "Number of days the license is valid. Leave empty for default (%s)."
msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s)." msgstr "Anzahl Tage, die die Lizenz gültig ist. Leer lassen für Standard (%s)."
#: src/Product/LicensedProductType.php:190 #. translators: %s: default bind to version value (Yes/No)
msgid "Bind to Major Version" #: src/Product/LicensedProductType.php:221
msgstr "An Hauptversion binden"
#: src/Product/LicensedProductType.php:193
#, php-format #, php-format
msgid "" msgid ""
"If enabled, licenses are bound to the major version at purchase time. " "If enabled, licenses are bound to the major version at purchase time. "
@@ -1500,57 +1539,38 @@ msgstr ""
"Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt " "Falls aktiviert, werden Lizenzen an die Hauptversion zum Kaufzeitpunkt "
"gebunden. Standard: %s" "gebunden. Standard: %s"
#: src/Product/LicensedProductType.php: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:327 #: src/Product/LicensedProductType.php:447
msgid "Version:" msgid "Version:"
msgstr "Version:" msgstr "Version:"
#: src/Product/LicensedProductType.php:355 #: 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:357 #: src/Product/LicensedProductType.php:525
msgid "Virtual" msgid "Virtual"
msgstr "Virtuell" msgstr "Virtuell"
#: src/Product/LicensedProductType.php:390 #: 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:399 #: 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:411 #: 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."
#: src/Product/LicensedProductVariation.php:143
msgid "Monthly"
msgstr "Monatlich"
#: src/Product/LicensedProductVariation.php:147
msgid "Quarterly"
msgstr "Vierteljährlich"
#: src/Product/LicensedProductVariation.php:151
msgid "Yearly"
msgstr "Jährlich"
#: src/Product/LicensedProductVariation.php:156
#, php-format
msgid "%d day"
msgid_plural "%d days"
msgstr[0] "%d Tag"
msgstr[1] "%d Tage"
#: src/Frontend/DownloadController.php:77 #: src/Frontend/DownloadController.php:77
#: src/Frontend/DownloadController.php:101 #: src/Frontend/DownloadController.php:101
msgid "Invalid download link." msgid "Invalid download link."
@@ -1610,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"
@@ -1767,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
@@ -1905,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."
@@ -1916,6 +1939,15 @@ msgstr ""
"WC Licensed Product benötigt WooCommerce als installierte und aktivierte " "WC Licensed Product benötigt WooCommerce als installierte und aktivierte "
"Erweiterung." "Erweiterung."
#~ msgid "Default Max Activations"
#~ msgstr "Standard Max. Aktivierungen"
#~ msgid "Default License Validity (Days)"
#~ msgstr "Standard Lizenz-Gültigkeit (Tage)"
#~ msgid "Default Bind to Major Version"
#~ msgstr "Standard An Hauptversion binden"
#~ msgid "API Verification Secret" #~ msgid "API Verification Secret"
#~ msgstr "API-Verifizierungs-Secret" #~ msgstr "API-Verifizierungs-Secret"
@@ -1932,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)."

View File

@@ -6,9 +6,9 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: WC Licensed Product 0.5.6\n" "Project-Id-Version: WC Licensed Product 0.6.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: magdev3.0@gmail.com\n"
"POT-Creation-Date: 2026-01-27 11:29+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"
@@ -303,10 +303,11 @@ msgstr ""
#: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613 #: src/Admin/AdminController.php:1373 src/Admin/AdminController.php:1613
#: src/Admin/DashboardWidgetController.php:136 #: src/Admin/DashboardWidgetController.php:136
#: src/Admin/OrderLicenseController.php:260 #: src/Admin/OrderLicenseController.php:260
#: src/Admin/SettingsController.php:192 src/Product/LicensedProductType.php:136 #: src/Admin/SettingsController.php:192
#: src/Product/LicensedProductType.php:184 #: src/Product/LicensedProductVariation.php:194
#: src/Product/LicensedProductType.php:385 #: src/Product/LicensedProductType.php:164
#: src/Product/LicensedProductVariation.php:139 #: src/Product/LicensedProductType.php:212
#: src/Product/LicensedProductType.php:553
#: src/Frontend/AccountController.php:286 #: src/Frontend/AccountController.php:286
msgid "Lifetime" msgid "Lifetime"
msgstr "" msgstr ""
@@ -484,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."
@@ -491,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."
@@ -498,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."
@@ -505,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."
@@ -512,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."
@@ -531,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."
@@ -538,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."
@@ -545,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."
@@ -552,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."
@@ -996,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."
@@ -1132,16 +1143,17 @@ msgid ""
"product settings override these defaults." "product settings override these defaults."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:176 #: src/Admin/SettingsController.php:176 src/Product/LicensedProductType.php:182
msgid "Default Max Activations" #: src/Product/LicensedProductType.php:570
msgid "Max Activations"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:178 #: src/Admin/SettingsController.php:178
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/Admin/SettingsController.php:187 src/Product/LicensedProductType.php:200
msgid "Default License Validity (Days)" msgid "License Validity (Days)"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:189 #: src/Admin/SettingsController.php:189
@@ -1150,8 +1162,8 @@ msgid ""
"lifetime licenses." "lifetime licenses."
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:199 #: src/Admin/SettingsController.php:199 src/Product/LicensedProductType.php:218
msgid "Default Bind to Major Version" msgid "Bind to Major Version"
msgstr "" msgstr ""
#: src/Admin/SettingsController.php:201 #: src/Admin/SettingsController.php:201
@@ -1174,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 ""
@@ -1297,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
@@ -1308,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 ""
@@ -1316,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 ""
@@ -1387,119 +1414,112 @@ 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/LicensedProductType.php:72 #: src/Product/LicensedProductVariation.php:198
msgid "Monthly"
msgstr ""
#: src/Product/LicensedProductVariation.php:202
msgid "Quarterly"
msgstr ""
#: src/Product/LicensedProductVariation.php:206
msgid "Yearly"
msgstr ""
#. translators: %d: number of days
#: src/Product/LicensedProductVariation.php:211
#, php-format
msgid "%d day"
msgid_plural "%d days"
msgstr[0] ""
msgstr[1] ""
#: 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:384 #: 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:154 #. translators: %d: default max activations value
#: src/Product/LicensedProductType.php:402 #: src/Product/LicensedProductType.php:185
msgid "Max Activations"
msgstr ""
#: src/Product/LicensedProductType.php:157
#, php-format #, php-format
msgid "Maximum number of domain activations per license. Default: %d" msgid "Maximum number of domain activations per license. Default: %d"
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php:172 #. translators: %s: default validity value
msgid "License Validity (Days)" #: src/Product/LicensedProductType.php:203
msgstr ""
#: src/Product/LicensedProductType.php:175
#, php-format #, php-format
msgid "Number of days the license is valid. Leave empty for default (%s)." msgid "Number of days the license is valid. Leave empty for default (%s)."
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php:190 #. translators: %s: default bind to version value (Yes/No)
msgid "Bind to Major Version" #: src/Product/LicensedProductType.php:221
msgstr ""
#: src/Product/LicensedProductType.php:193
#, php-format #, php-format
msgid "" msgid ""
"If enabled, licenses are bound to the major version at purchase time. " "If enabled, licenses are bound to the major version at purchase time. "
"Default: %s" "Default: %s"
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php: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:327 #: src/Product/LicensedProductType.php:447
msgid "Version:" msgid "Version:"
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php:355 #: src/Product/LicensedProductType.php:523
msgid "Licensed products are always virtual" msgid "Licensed products are always virtual"
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php:357 #: src/Product/LicensedProductType.php:525
msgid "Virtual" msgid "Virtual"
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php:390 #: src/Product/LicensedProductType.php:558
msgid "License Duration (Days)" msgid "License Duration (Days)"
msgstr "" msgstr ""
#: src/Product/LicensedProductType.php:399 #: 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:411 #: src/Product/LicensedProductType.php:579
msgid "Leave empty for parent default." msgid "Leave empty for parent default."
msgstr "" msgstr ""
#: src/Product/LicensedProductVariation.php:143
msgid "Monthly"
msgstr ""
#: src/Product/LicensedProductVariation.php:147
msgid "Quarterly"
msgstr ""
#: src/Product/LicensedProductVariation.php:151
msgid "Yearly"
msgstr ""
#: src/Product/LicensedProductVariation.php:156
#, php-format
msgid "%d day"
msgid_plural "%d days"
msgstr[0] ""
msgstr[1] ""
#: src/Frontend/DownloadController.php:77 #: src/Frontend/DownloadController.php:77
#: src/Frontend/DownloadController.php:101 #: src/Frontend/DownloadController.php:101
msgid "Invalid download link." msgid "Invalid download link."
@@ -1559,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"
@@ -1708,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
@@ -1838,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."
@@ -1846,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 ""

View File

@@ -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"
} }
] ]
} }

Binary file not shown.

View File

@@ -0,0 +1 @@
2bbc0655f724e201367247f0e40974ddce6d7c559987e661f2b06b43294fc99f wc-licensed-product-0.5.10.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
32571178bfa8f0d0a03ed05b498d5f9b3c860104393a96732e86a03b6de298d2 wc-licensed-product-0.5.11.zip

View File

@@ -0,0 +1 @@
20bb5cd453de9bca781864430ebd152c82f660b6f9fc3f09107ba03489a71d75 /home/magdev/workspaces/php/wordpress/wp-content/plugins/wc-licensed-product/releases/wc-licensed-product-0.5.12.zip

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
814710ad899529d0015494e4b332eace7d8e55aeda381fdf61f99274c0bf910c wc-licensed-product-0.5.13.zip

View File

@@ -0,0 +1 @@
47407de49bae4c649644af64e87b44b32fb30eeb2d50890ff8c4bbb741059278 wc-licensed-product-0.5.15.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
4d35a319fe4cb4e7055bae17fc030487ca05e5e9ac905f76d0ac62002bde4336 releases/wc-licensed-product-0.5.6.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
ceb4d57598f576f4f172153ff80df8c180ecd4dca873cf109327fc5ac718930f wc-licensed-product-0.5.7.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
670c2f5182ea7140ccf9533c2b4179daf7890019a244973f467f2a5c7622b9f4 wc-licensed-product-0.5.8.zip

Binary file not shown.

View File

@@ -0,0 +1 @@
fae77dab56cb8f46693cf44fe6a1dc38ad0526d881cab2cd1f0878b234afaa8b wc-licensed-product-0.5.9.zip

View File

@@ -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;
} }

View File

@@ -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
*/ */
@@ -173,7 +213,7 @@ final class SettingsController
'id' => 'wc_licensed_product_section_defaults', 'id' => 'wc_licensed_product_section_defaults',
], ],
'default_max_activations' => [ 'default_max_activations' => [
'name' => __('Default Max Activations', 'wc-licensed-product'), 'name' => __('Max Activations', 'wc-licensed-product'),
'type' => 'number', 'type' => 'number',
'desc' => __('Default maximum number of domain activations per license.', 'wc-licensed-product'), 'desc' => __('Default maximum number of domain activations per license.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_default_max_activations', 'id' => 'wc_licensed_product_default_max_activations',
@@ -184,7 +224,7 @@ final class SettingsController
], ],
], ],
'default_validity_days' => [ 'default_validity_days' => [
'name' => __('Default License Validity (Days)', 'wc-licensed-product'), 'name' => __('License Validity (Days)', 'wc-licensed-product'),
'type' => 'number', 'type' => 'number',
'desc' => __('Default number of days a license is valid. Leave empty or set to 0 for lifetime licenses.', 'wc-licensed-product'), 'desc' => __('Default number of days a license is valid. Leave empty or set to 0 for lifetime licenses.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_default_validity_days', 'id' => 'wc_licensed_product_default_validity_days',
@@ -196,7 +236,7 @@ final class SettingsController
], ],
], ],
'default_bind_to_version' => [ 'default_bind_to_version' => [
'name' => __('Default Bind to Major Version', 'wc-licensed-product'), 'name' => __('Bind to Major Version', 'wc-licensed-product'),
'type' => 'checkbox', 'type' => 'checkbox',
'desc' => __('If enabled, licenses are bound to the major version at purchase time by default.', 'wc-licensed-product'), 'desc' => __('If enabled, licenses are bound to the major version at purchase time by default.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_default_bind_to_version', 'id' => 'wc_licensed_product_default_bind_to_version',
@@ -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
*/ */

View File

@@ -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')]);
} }

View 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');
}
}

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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
*/ */

View File

@@ -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;
} }

View File

@@ -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();

View File

@@ -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

View File

@@ -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,33 +247,6 @@ final class LicensedProductType
?> ?>
</div> </div>
</div> </div>
<script type="text/javascript">
jQuery(document).ready(function($) {
// Show/hide panels based on product type
function toggleLicensedProductOptions() {
var productType = $('#product-type').val();
var isLicensed = productType === 'licensed';
var isLicensedVariable = productType === 'licensed-variable';
if (isLicensed || isLicensedVariable) {
$('.show_if_licensed').show();
$('.show_if_licensed-variable').show();
$('.general_options').show();
$('.pricing').show();
$('.general_tab').show();
} else {
$('.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
} }
@@ -263,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
{ {
@@ -302,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');
}
} }
/** /**
@@ -331,10 +442,58 @@ 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
{ {
wc_get_template('single-product/add-to-cart/variable.php'); global $product;
// 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;
}
// 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
$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(
'single-product/add-to-cart/variable.php',
[
'available_variations' => $availableVariations,
'attributes' => $attributes,
'selected_attributes' => $selectedAttributes,
]
);
} }
/** /**
@@ -474,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
{ {
@@ -491,34 +651,62 @@ 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();
// Hide shipping tab (virtual products)
$('.shipping_tab').hide();
} }
} }
// 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() {
toggleLicensedVariableOptions(); setTimeout(toggleOurElements, 100);
});
// Re-apply after WooCommerce AJAX operations
$(document).on('woocommerce_variations_loaded woocommerce_variations_added woocommerce_variations_saved', function() {
setTimeout(toggleOurElements, 10);
}); });
}); });
</script> </script>

View File

@@ -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

View File

@@ -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;
} }
/** /**

View 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);
}
}

View File

@@ -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.6 * 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.6'); 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__));