Implement versions 0.0.4-0.0.7 features

v0.0.4:
- Add WooCommerce settings tab for default license settings
- Per-product settings override global defaults

v0.0.5:
- Add bulk license operations (activate, deactivate, revoke, extend, delete)
- Add license renewal/extension and lifetime functionality
- Add quick action buttons per license row

v0.0.6:
- Add license dashboard with statistics and analytics
- Add license transfer functionality (admin)
- Add CSV export for licenses
- Add OpenAPI 3.1 specification
- Remove /deactivate API endpoint

v0.0.7:
- Move license dashboard to WooCommerce Reports section
- Add license search and filtering in admin
- Add customer-facing license transfer with AJAX modal
- Add email notifications for license expiration warnings
- Add bulk import licenses from CSV
- Update README with comprehensive documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 20:32:35 +01:00
parent 78e43b9aea
commit 49a0699963
21 changed files with 4132 additions and 289 deletions

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Product;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
use WC_Product;
/**
@@ -55,28 +56,68 @@ class LicensedProduct extends WC_Product
/**
* Get max activations for this product
* Falls back to default settings if not set on product
*/
public function get_max_activations(): int
{
$value = $this->get_meta('_licensed_max_activations', true);
return $value ? (int) $value : 1;
if ($value !== '' && $value !== null) {
return max(1, (int) $value);
}
return SettingsController::getDefaultMaxActivations();
}
/**
* Check if product has custom max activations set
*/
public function has_custom_max_activations(): bool
{
$value = $this->get_meta('_licensed_max_activations', true);
return $value !== '' && $value !== null;
}
/**
* Get validity days
* Falls back to default settings if not set on product
*/
public function get_validity_days(): ?int
{
$value = $this->get_meta('_licensed_validity_days', true);
return $value !== '' ? (int) $value : null;
if ($value !== '' && $value !== null) {
return (int) $value > 0 ? (int) $value : null;
}
return SettingsController::getDefaultValidityDays();
}
/**
* Check if product has custom validity days set
*/
public function has_custom_validity_days(): bool
{
$value = $this->get_meta('_licensed_validity_days', true);
return $value !== '' && $value !== null;
}
/**
* Check if license should be bound to major version
* Falls back to default settings if not set on product
*/
public function is_bound_to_version(): bool
{
return $this->get_meta('_licensed_bind_to_version', true) === 'yes';
$value = $this->get_meta('_licensed_bind_to_version', true);
if ($value !== '' && $value !== null) {
return $value === 'yes';
}
return SettingsController::getDefaultBindToVersion();
}
/**
* Check if product has custom bind to version setting
*/
public function has_custom_bind_to_version(): bool
{
$value = $this->get_meta('_licensed_bind_to_version', true);
return $value !== '' && $value !== null;
}
/**

View File

@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Product;
use Jeremias\WcLicensedProduct\Admin\SettingsController;
/**
* Registers and handles the Licensed product type for WooCommerce
*/
@@ -85,39 +87,82 @@ final class LicensedProductType
public function addProductDataPanel(): void
{
global $post;
// Get current product values
$currentMaxActivations = get_post_meta($post->ID, '_licensed_max_activations', true);
$currentValidityDays = get_post_meta($post->ID, '_licensed_validity_days', true);
$currentBindToVersion = get_post_meta($post->ID, '_licensed_bind_to_version', true);
// Get default values
$defaultMaxActivations = SettingsController::getDefaultMaxActivations();
$defaultValidityDays = SettingsController::getDefaultValidityDays();
$defaultBindToVersion = SettingsController::getDefaultBindToVersion();
// Format default validity for display
$defaultValidityDisplay = $defaultValidityDays !== null
? sprintf(__('%d days', 'wc-licensed-product'), $defaultValidityDays)
: __('Lifetime', 'wc-licensed-product');
?>
<div id="licensed_product_data" class="panel woocommerce_options_panel hidden">
<div class="options_group">
<p class="form-field">
<em><?php
printf(
/* translators: %s: URL to settings page */
esc_html__('Leave fields empty to use default settings from %s.', 'wc-licensed-product'),
'<a href="' . esc_url(admin_url('admin.php?page=wc-settings&tab=licensed_product')) . '">' .
esc_html__('WooCommerce > Settings > Licensed Products', 'wc-licensed-product') . '</a>'
);
?></em>
</p>
<?php
woocommerce_wp_text_input([
'id' => '_licensed_max_activations',
'label' => __('Max Activations', 'wc-licensed-product'),
'description' => __('Maximum number of domain activations per license. Default: 1', 'wc-licensed-product'),
'description' => sprintf(
/* translators: %d: default max activations value */
__('Maximum number of domain activations per license. Default: %d', 'wc-licensed-product'),
$defaultMaxActivations
),
'desc_tip' => true,
'type' => 'number',
'custom_attributes' => [
'min' => '1',
'step' => '1',
],
'value' => get_post_meta($post->ID, '_licensed_max_activations', true) ?: '1',
'placeholder' => (string) $defaultMaxActivations,
'value' => $currentMaxActivations,
]);
woocommerce_wp_text_input([
'id' => '_licensed_validity_days',
'label' => __('License Validity (Days)', 'wc-licensed-product'),
'description' => __('Number of days the license is valid. Leave empty for lifetime license.', 'wc-licensed-product'),
'description' => sprintf(
/* translators: %s: default validity value */
__('Number of days the license is valid. Leave empty for default (%s).', 'wc-licensed-product'),
$defaultValidityDisplay
),
'desc_tip' => true,
'type' => 'number',
'custom_attributes' => [
'min' => '0',
'step' => '1',
],
'placeholder' => $defaultValidityDays !== null ? (string) $defaultValidityDays : __('Lifetime', 'wc-licensed-product'),
'value' => $currentValidityDays,
]);
woocommerce_wp_checkbox([
'id' => '_licensed_bind_to_version',
'label' => __('Bind to Major Version', 'wc-licensed-product'),
'description' => __('If enabled, licenses are bound to the major version at purchase time.', 'wc-licensed-product'),
'description' => sprintf(
/* translators: %s: default bind to version value (Yes/No) */
__('If enabled, licenses are bound to the major version at purchase time. Default: %s', 'wc-licensed-product'),
$defaultBindToVersion ? __('Yes', 'wc-licensed-product') : __('No', 'wc-licensed-product')
),
'value' => $currentBindToVersion ?: ($defaultBindToVersion ? 'yes' : 'no'),
'cbvalue' => 'yes',
]);
woocommerce_wp_text_input([
@@ -160,19 +205,26 @@ final class LicensedProductType
public function saveProductMeta(int $postId): void
{
// Verify nonce is handled by WooCommerce
$maxActivations = isset($_POST['_licensed_max_activations'])
// Allow empty values to fall back to defaults
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified by WooCommerce
$maxActivations = isset($_POST['_licensed_max_activations']) && $_POST['_licensed_max_activations'] !== ''
? absint($_POST['_licensed_max_activations'])
: 1;
: '';
update_post_meta($postId, '_licensed_max_activations', $maxActivations);
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$validityDays = isset($_POST['_licensed_validity_days']) && $_POST['_licensed_validity_days'] !== ''
? absint($_POST['_licensed_validity_days'])
: '';
update_post_meta($postId, '_licensed_validity_days', $validityDays);
// For checkbox, we need to distinguish between "not set" and "explicitly unchecked"
// If the hidden field is present, the form was submitted and we save the actual value
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$bindToVersion = isset($_POST['_licensed_bind_to_version']) ? 'yes' : 'no';
update_post_meta($postId, '_licensed_bind_to_version', $bindToVersion);
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$currentVersion = isset($_POST['_licensed_current_version'])
? sanitize_text_field($_POST['_licensed_current_version'])
: '';