From 7bbffa50b44e8ee533e8fe3106d71ee3bc8e581c Mon Sep 17 00:00:00 2001 From: magdev Date: Tue, 27 Jan 2026 21:22:45 +0100 Subject: [PATCH] Release v0.6.1 - UI improvements and bug fixes - Fix admin license test popup showing empty product field - Display product name in bold in test license modal - Split auto-update settings into notification and auto-install options - Add filter functionality to customer account licenses page - Update translations (402 strings) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 36 +++++++ CLAUDE.md | 52 ++++++++- assets/css/frontend.css | 74 +++++++++++++ composer.lock | 4 +- src/Admin/AdminController.php | 18 +++- src/Admin/SettingsController.php | 45 ++++++-- src/Frontend/AccountController.php | 148 +++++++++++++++++++++++++- src/Update/PluginUpdateChecker.php | 34 ++++-- templates/admin/licenses.html.twig | 5 +- templates/frontend/licenses.html.twig | 50 ++++++++- wc-licensed-product.php | 4 +- 11 files changed, 441 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c896a..5e287fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.1] - 2026-01-27 + +### Added + +- Filter functionality on customer account licenses page (filter by product or domain) +- Split auto-update settings into two options: "Enable Update Notifications" and "Automatically Install Updates" +- New `isUpdateNotificationEnabled()`, `isAutoInstallEnabled()` static methods in SettingsController +- WordPress auto-update filter integration (`auto_update_plugin`) for automatic installation + +### Fixed + +- Fixed admin license test popup showing empty product field +- `handleAjaxTestLicense()` now enriches response with product name +- Removed version field from test popup (version_id is only set for version-bound licenses) + +### Changed + +- Updated `magdev/wc-licensed-product-client` dependency to v0.2.1 +- "Automatically Install Updates" is only selectable when "Enable Update Notifications" is enabled + +## [0.6.0] - 2026-01-27 + +### Added + +- WordPress-style automatic update system for licensed plugins +- Server-side `/update-check` REST API endpoint for WordPress-compatible update information +- Client-side `PluginUpdateChecker` singleton for WordPress update integration +- New "Auto-Updates" settings subtab with enable/disable and check frequency options +- Secure download authentication via `X-License-Key` header +- Response signing support for tamper-proof update responses +- Configurable cache TTL for update checks (1-168 hours) + +### Changed + +- Updated OpenAPI specification to version 0.6.0 with `/update-check` endpoint documentation + ## [0.5.15] - 2026-01-27 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index cb059b4..15bca87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w ### Version 0.7.0 -*No planned features yet.* +No changes planned at the moment ## Technical Stack @@ -1732,3 +1732,53 @@ define('WC_LICENSE_DISABLE_AUTO_UPDATE', true); - Created release package: `releases/wc-licensed-product-0.6.0.zip` (1.1 MB) - SHA256: `171c8195c586b3b20bac4a806e2d698cdaaf15966e2fd6e1670ec39dac8ab027` - Tagged as `v0.6.0` and pushed to `main` branch + +### 2026-01-27 - Version 0.6.1 - UI Improvements & Bug Fixes + +**Overview:** + +Bug fix and improvement release addressing admin license testing, auto-update settings, and customer license filtering. + +**Implemented:** + +- Filter functionality on customer account licenses page (filter by product or domain) +- Split auto-update settings into "Enable Update Notifications" and "Automatically Install Updates" +- WordPress `auto_update_plugin` filter integration for automatic installation + +**Bug Fixes:** + +- Fixed admin license test popup showing empty product field +- Removed version field from test popup (version_id is only set for version-bound licenses) +- `handleAjaxTestLicense()` now enriches response with product name + +**Modified files:** + +- `src/Admin/AdminController.php` - Enriched test license response with product name +- `src/Admin/SettingsController.php` - Split auto-update settings, added static helper methods +- `src/Update/PluginUpdateChecker.php` - Added `auto_update_plugin` filter, use new settings methods +- `src/Frontend/AccountController.php` - Added filter functionality with `applyLicenseFilters()` method +- `templates/frontend/licenses.html.twig` - Added filter form with product and domain dropdowns +- `templates/admin/licenses.html.twig` - Removed version row from test license modal +- `assets/css/frontend.css` - Added responsive styles for filter form +- `languages/*` - Updated all translation files + +**New methods in SettingsController:** + +- `isUpdateNotificationEnabled()` - Check if update notifications are enabled +- `isAutoInstallEnabled()` - Check if auto-install is enabled (requires notifications enabled) + +**New methods in AccountController:** + +- `applyLicenseFilters()` - Filter licenses by product ID and/or domain +- `getFilterOptions()` - Get unique products and domains for filter dropdowns + +**Technical notes:** + +- Filter form uses GET parameters: `filter_product` and `filter_domain` +- Auto-install setting is disabled (greyed out) when update notifications are disabled +- License test popup now only shows Product and Expires fields (version removed) +- Domain filter uses case-insensitive partial matching via `stripos()` + +**Dependency Updates:** + +- Updated `magdev/wc-licensed-product-client` from v0.2.0 to v0.2.1 diff --git a/assets/css/frontend.css b/assets/css/frontend.css index 4ec55ac..2a4c10d 100644 --- a/assets/css/frontend.css +++ b/assets/css/frontend.css @@ -37,6 +37,80 @@ color: #383d41; } +/* Filter Form */ +.wclp-filter-form { + margin-bottom: 1.5em; + padding: 1em; + background-color: #f8f9fa; + border: 1px solid #e5e5e5; + border-radius: 8px; +} + +.wclp-filter-row { + display: flex; + flex-wrap: wrap; + gap: 1em; + align-items: flex-end; +} + +.wclp-filter-field { + display: flex; + flex-direction: column; + gap: 0.3em; + flex: 1; + min-width: 150px; +} + +.wclp-filter-field label { + font-size: 0.85em; + font-weight: 600; + color: #666; +} + +.wclp-filter-field select { + width: 100%; + padding: 0.5em 0.75em; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #fff; + font-size: 0.95em; +} + +.wclp-filter-field select:focus { + border-color: #0073aa; + outline: none; + box-shadow: 0 0 0 1px #0073aa; +} + +.wclp-filter-actions { + display: flex; + gap: 0.5em; +} + +.wclp-filter-actions .button { + padding: 0.5em 1em; + font-size: 0.95em; + white-space: nowrap; +} + +@media (max-width: 600px) { + .wclp-filter-row { + flex-direction: column; + } + + .wclp-filter-field { + min-width: 100%; + } + + .wclp-filter-actions { + width: 100%; + } + + .wclp-filter-actions .button { + flex: 1; + } +} + /* License Packages */ .woocommerce-licenses { display: flex; diff --git a/composer.lock b/composer.lock index f5a9061..bc336a8 100644 --- a/composer.lock +++ b/composer.lock @@ -12,7 +12,7 @@ "source": { "type": "git", "url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git", - "reference": "5e4b5a970f75d0163c5496581d963a24ade4f276" + "reference": "760e1e752a0c088fa634cf7ff678e0735ed525a4" }, "require": { "php": "^8.3", @@ -52,7 +52,7 @@ "issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues", "source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client" }, - "time": "2026-01-26T15:54:37+00:00" + "time": "2026-01-27T19:52:12+00:00" }, { "name": "psr/cache", diff --git a/src/Admin/AdminController.php b/src/Admin/AdminController.php index 4903a2b..7bd6521 100644 --- a/src/Admin/AdminController.php +++ b/src/Admin/AdminController.php @@ -379,6 +379,19 @@ final class AdminController // Validate the license using LicenseManager $result = $this->licenseManager->validateLicense($licenseKey, $domain); + // Enrich result with product name for display in the popup + if (!empty($result['valid']) && isset($result['license'])) { + // Get product name + $productId = $result['license']['product_id'] ?? null; + if ($productId) { + $product = wc_get_product($productId); + $result['product_name'] = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'); + } + + // Flatten expires_at for easier access in JavaScript + $result['expires_at'] = $result['license']['expires_at'] ?? null; + } + wp_send_json_success($result); } @@ -1605,12 +1618,11 @@ final class AdminController if (result.valid) { html = '

'; html += ''; - html += ''; - html += ''; + html += ''; if (result.expires_at) { html += ''; } else { - html += ''; + html += ''; } html += '
' + escapeHtml(result.product_name || '-') + '
' + escapeHtml(result.version || '-') + '
' + escapeHtml(result.product_name || '-') + '
' + escapeHtml(result.expires_at) + '
'; } else { diff --git a/src/Admin/SettingsController.php b/src/Admin/SettingsController.php index ca2b254..0d778bb 100644 --- a/src/Admin/SettingsController.php +++ b/src/Admin/SettingsController.php @@ -167,6 +167,8 @@ final class SettingsController */ private function getAutoUpdatesSettings(): array { + $autoInstallDisabled = !self::isUpdateNotificationEnabled(); + return [ 'auto_update_section_title' => [ 'name' => __('Auto-Updates', 'wc-licensed-product'), @@ -174,13 +176,23 @@ final class SettingsController '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'), + 'update_notification_enabled' => [ + 'name' => __('Enable Update Notifications', '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', + 'desc' => __('Check for and display available updates from the license server in WordPress admin.', 'wc-licensed-product'), + 'id' => 'wc_licensed_product_update_notification_enabled', 'default' => 'yes', ], + 'plugin_auto_install_enabled' => [ + 'name' => __('Automatically Install Updates', 'wc-licensed-product'), + 'type' => 'checkbox', + 'desc' => $autoInstallDisabled + ? __('Enable "Update Notifications" above to use this option.', 'wc-licensed-product') + : __('Automatically install updates when they become available (requires update notifications enabled).', 'wc-licensed-product'), + 'id' => 'wc_licensed_product_plugin_auto_install_enabled', + 'default' => 'no', + 'custom_attributes' => $autoInstallDisabled ? ['disabled' => 'disabled'] : [], + ], 'update_check_frequency' => [ 'name' => __('Check Frequency (Hours)', 'wc-licensed-product'), 'type' => 'number', @@ -501,11 +513,32 @@ final class SettingsController } /** - * Check if auto-updates are enabled + * Check if update notifications are enabled + */ + public static function isUpdateNotificationEnabled(): bool + { + return get_option('wc_licensed_product_update_notification_enabled', 'yes') === 'yes'; + } + + /** + * Check if auto-updates are enabled (legacy alias for isUpdateNotificationEnabled) */ public static function isAutoUpdateEnabled(): bool { - return get_option('wc_licensed_product_plugin_auto_update_enabled', 'yes') === 'yes'; + return self::isUpdateNotificationEnabled(); + } + + /** + * Check if automatic installation of updates is enabled + */ + public static function isAutoInstallEnabled(): bool + { + // Auto-install requires notifications to be enabled first + if (!self::isUpdateNotificationEnabled()) { + return false; + } + + return get_option('wc_licensed_product_plugin_auto_install_enabled', 'no') === 'yes'; } /** diff --git a/src/Frontend/AccountController.php b/src/Frontend/AccountController.php index 85b165b..0d4aafe 100644 --- a/src/Frontend/AccountController.php +++ b/src/Frontend/AccountController.php @@ -106,23 +106,104 @@ final class AccountController return; } + // Get filter parameters from URL + $filterProductId = isset($_GET['filter_product']) ? absint($_GET['filter_product']) : 0; + $filterDomain = isset($_GET['filter_domain']) ? sanitize_text_field(wp_unslash($_GET['filter_domain'])) : ''; + $licenses = $this->licenseManager->getLicensesByCustomer($customerId); + // Apply filters + $filteredLicenses = $this->applyLicenseFilters($licenses, $filterProductId, $filterDomain); + // Group licenses by product+order into "packages" - $packages = $this->groupLicensesIntoPackages($licenses); + $packages = $this->groupLicensesIntoPackages($filteredLicenses); + + // Get unique products and domains for filter dropdowns + $filterOptions = $this->getFilterOptions($licenses); try { echo $this->twig->render('frontend/licenses.html.twig', [ 'packages' => $packages, 'has_packages' => !empty($packages), 'signing_enabled' => ResponseSigner::isSigningEnabled(), + 'filter_products' => $filterOptions['products'], + 'filter_domains' => $filterOptions['domains'], + 'current_filter_product' => $filterProductId, + 'current_filter_domain' => $filterDomain, + 'is_filtered' => $filterProductId > 0 || !empty($filterDomain), + 'licenses_url' => wc_get_account_endpoint_url('licenses'), ]); } catch (\Exception $e) { // Fallback to PHP template if Twig fails - $this->displayLicensesFallback($packages); + $this->displayLicensesFallback($packages, $filterOptions, $filterProductId, $filterDomain); } } + /** + * Apply filters to licenses + * + * @param array $licenses Array of License objects + * @param int $productId Filter by product ID (0 for all) + * @param string $domain Filter by domain (empty for all) + * @return array Filtered array of License objects + */ + private function applyLicenseFilters(array $licenses, int $productId, string $domain): array + { + if ($productId === 0 && empty($domain)) { + return $licenses; + } + + return array_filter($licenses, function ($license) use ($productId, $domain) { + // Filter by product + if ($productId > 0 && $license->getProductId() !== $productId) { + return false; + } + + // Filter by domain (partial match) + if (!empty($domain) && stripos($license->getDomain(), $domain) === false) { + return false; + } + + return true; + }); + } + + /** + * Get unique filter options from licenses + * + * @param array $licenses Array of License objects + * @return array Array with 'products' and 'domains' keys + */ + private function getFilterOptions(array $licenses): array + { + $products = []; + $domains = []; + + foreach ($licenses as $license) { + // Collect unique products + $productId = $license->getProductId(); + if (!isset($products[$productId])) { + $product = wc_get_product($productId); + $products[$productId] = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'); + } + + // Collect unique domains + $domain = $license->getDomain(); + if (!in_array($domain, $domains, true)) { + $domains[] = $domain; + } + } + + // Sort products by name, domains alphabetically + asort($products); + sort($domains); + + return [ + 'products' => $products, + 'domains' => $domains, + ]; + } + /** * Group licenses into packages by product+order * @@ -217,10 +298,67 @@ final class AccountController /** * Fallback display method if Twig is unavailable */ - private function displayLicensesFallback(array $packages): void - { + private function displayLicensesFallback( + array $packages, + array $filterOptions = [], + int $currentFilterProduct = 0, + string $currentFilterDomain = '' + ): void { + $isFiltered = $currentFilterProduct > 0 || !empty($currentFilterDomain); + $licensesUrl = wc_get_account_endpoint_url('licenses'); + + // Display filter form if we have filter options + if (!empty($filterOptions['products']) || !empty($filterOptions['domains'])) { + ?> +
+
+
+ +
+ + +
+ + + +
+ + +
+ + +
+ + + + +
+
+
+
+ ' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '

'; + if ($isFiltered) { + echo '

' . esc_html__('No licenses found matching your filters.', 'wc-licensed-product') . '

'; + } else { + echo '

' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '

'; + } return; } diff --git a/src/Update/PluginUpdateChecker.php b/src/Update/PluginUpdateChecker.php index fb7974c..2062897 100644 --- a/src/Update/PluginUpdateChecker.php +++ b/src/Update/PluginUpdateChecker.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Jeremias\WcLicensedProduct\Update; +use Jeremias\WcLicensedProduct\Admin\SettingsController; use Jeremias\WcLicensedProduct\License\PluginLicenseChecker; use Symfony\Component\HttpClient\HttpClient; @@ -74,8 +75,8 @@ final class PluginUpdateChecker */ public function register(): void { - // Skip if auto-updates are disabled - if ($this->isAutoUpdateDisabled()) { + // Skip if update notifications are disabled + if ($this->isUpdateNotificationDisabled()) { return; } @@ -88,15 +89,19 @@ final class PluginUpdateChecker // Add authentication headers to download requests add_filter('http_request_args', [$this, 'addAuthHeaders'], 10, 2); + // Handle auto-install setting + add_filter('auto_update_plugin', [$this, 'handleAutoInstall'], 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']); + add_action('update_option_wc_licensed_product_update_notification_enabled', [$this, 'clearCache']); } /** - * Check if auto-updates are disabled + * Check if update notifications are disabled */ - private function isAutoUpdateDisabled(): bool + private function isUpdateNotificationDisabled(): bool { // Check constant if (defined('WC_LICENSE_DISABLE_AUTO_UPDATE') && WC_LICENSE_DISABLE_AUTO_UPDATE) { @@ -104,8 +109,25 @@ final class PluginUpdateChecker } // Check setting - $enabled = get_option('wc_licensed_product_plugin_auto_update_enabled', 'yes'); - return $enabled !== 'yes'; + return !SettingsController::isUpdateNotificationEnabled(); + } + + /** + * Handle auto-install setting for WordPress automatic updates + * + * @param bool|null $update The update decision + * @param object $item The plugin update object + * @return bool|null Whether to auto-update this plugin + */ + public function handleAutoInstall($update, $item): ?bool + { + // Only handle our plugin + if (!isset($item->plugin) || $item->plugin !== $this->pluginBasename) { + return $update; + } + + // Return true to enable auto-install, false to disable, or null to use default + return SettingsController::isAutoInstallEnabled() ? true : $update; } /** diff --git a/templates/admin/licenses.html.twig b/templates/admin/licenses.html.twig index 38af54f..d121fa7 100644 --- a/templates/admin/licenses.html.twig +++ b/templates/admin/licenses.html.twig @@ -424,12 +424,11 @@ if (result.valid) { html = '

✓ {{ __('License is VALID') }}

'; html += ''; - html += ''; - html += ''; + html += ''; if (result.expires_at) { html += ''; } else { - html += ''; + html += ''; } html += '
{{ __('Product') }}' + escapeHtml(result.product_name || '-') + '
{{ __('Version') }}' + escapeHtml(result.version || '-') + '
{{ __('Product') }}' + escapeHtml(result.product_name || '-') + '
{{ __('Expires') }}' + escapeHtml(result.expires_at) + '
{{ __('Expires') }}{{ __('Lifetime') }}
{{ __('Expires') }}{{ __('Lifetime') }}
'; } else { diff --git a/templates/frontend/licenses.html.twig b/templates/frontend/licenses.html.twig index f0a3f67..23af355 100644 --- a/templates/frontend/licenses.html.twig +++ b/templates/frontend/licenses.html.twig @@ -1,5 +1,53 @@ +{# License Filter Form #} +{% if filter_products is defined and filter_products|length > 0 or filter_domains is defined and filter_domains|length > 0 %} +
+
+
+ {% if filter_products is defined and filter_products|length > 0 %} +
+ + +
+ {% endif %} + + {% if filter_domains is defined and filter_domains|length > 0 %} +
+ + +
+ {% endif %} + +
+ + {% if is_filtered %} + {{ __('Clear') }} + {% endif %} +
+
+
+
+{% endif %} + {% if not has_packages %} -

{{ __('You have no licenses yet.') }}

+ {% if is_filtered %} +

{{ __('No licenses found matching your filters.') }}

+ {% else %} +

{{ __('You have no licenses yet.') }}

+ {% endif %} {% else %}
{% for package in packages %} diff --git a/wc-licensed-product.php b/wc-licensed-product.php index e2b5fc4..1cbb77d 100644 --- a/wc-licensed-product.php +++ b/wc-licensed-product.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce 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. - * Version: 0.6.0 + * Version: 0.6.1 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * License: GPL-2.0-or-later @@ -28,7 +28,7 @@ if (!defined('ABSPATH')) { } // Plugin constants -define('WC_LICENSED_PRODUCT_VERSION', '0.6.0'); +define('WC_LICENSED_PRODUCT_VERSION', '0.6.1'); define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__); define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));