You've already forked wc-licensed-product
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff0229061d | |||
| 7bbffa50b4 | |||
| e168b1a44b | |||
| eb8818aa81 | |||
| fddeda4a80 |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.5.15] - 2026-01-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
144
CLAUDE.md
144
CLAUDE.md
@@ -32,9 +32,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||||
|
|
||||||
### Version 0.6.0
|
### Version 0.7.0
|
||||||
|
|
||||||
*No planned features yet.*
|
No changes planned at the moment
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -197,11 +197,13 @@ wc-licensed-product/
|
|||||||
├── releases/ # Release packages (version 0.1.0+)
|
├── releases/ # Release packages (version 0.1.0+)
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── Admin/ # AdminController - license management UI
|
│ ├── Admin/ # AdminController - license management UI
|
||||||
│ ├── Api/ # RestApiController - license validation endpoints
|
│ ├── Api/ # RestApiController, UpdateController - REST API endpoints
|
||||||
│ ├── Checkout/ # CheckoutController - domain field at checkout
|
│ ├── Checkout/ # CheckoutController - domain field at checkout
|
||||||
|
│ ├── Email/ # WooCommerce email classes
|
||||||
│ ├── Frontend/ # AccountController - customer licenses page
|
│ ├── Frontend/ # AccountController - customer licenses page
|
||||||
│ ├── License/ # License model and LicenseManager
|
│ ├── License/ # License model and LicenseManager
|
||||||
│ └── Product/ # LicensedProduct type and LicensedProductType
|
│ ├── Product/ # LicensedProduct type and LicensedProductType
|
||||||
|
│ └── Update/ # PluginUpdateChecker - WordPress auto-update integration
|
||||||
├── templates/
|
├── templates/
|
||||||
│ ├── admin/ # Twig templates for admin views
|
│ ├── admin/ # Twig templates for admin views
|
||||||
│ └── frontend/ # Twig templates for customer views
|
│ └── frontend/ # Twig templates for customer views
|
||||||
@@ -219,11 +221,12 @@ Created on plugin activation via `Installer::createTables()`:
|
|||||||
|
|
||||||
Base: `/wp-json/wc-licensed-product/v1/`
|
Base: `/wp-json/wc-licensed-product/v1/`
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
| Endpoint | Method | Description |
|
||||||
| ----------- | ------ | ------------------------------- |
|
| --------------- | ------ | ---------------------------------- |
|
||||||
| `/validate` | POST | Validate license key for domain |
|
| `/validate` | POST | Validate license key for domain |
|
||||||
| `/status` | POST | Get license status |
|
| `/status` | POST | Get license status |
|
||||||
| `/activate` | POST | Activate license on domain |
|
| `/activate` | POST | Activate license on domain |
|
||||||
|
| `/update-check` | POST | Check for plugin updates (v0.6.0+) |
|
||||||
|
|
||||||
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
||||||
|
|
||||||
@@ -1656,3 +1659,126 @@ Fixed tab rendering bug in WooCommerce product edit page when switching to licen
|
|||||||
- Created release package: `releases/wc-licensed-product-0.5.15.zip` (862 KB)
|
- Created release package: `releases/wc-licensed-product-0.5.15.zip` (862 KB)
|
||||||
- SHA256: `47407de49bae4c649644af64e87b44b32fb30eeb2d50890ff8c4bbb741059278`
|
- SHA256: `47407de49bae4c649644af64e87b44b32fb30eeb2d50890ff8c4bbb741059278`
|
||||||
- Committed to `dev` branch
|
- Committed to `dev` branch
|
||||||
|
|
||||||
|
### 2026-01-27 - Version 0.6.0 - WordPress Auto-Update System
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Major feature release implementing WordPress-style automatic updates. Licensed plugins can now receive updates through WordPress's native plugin update mechanism by checking against the license server.
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
- `src/Api/UpdateController.php` - Server-side REST API endpoint for update checks
|
||||||
|
- `src/Update/PluginUpdateChecker.php` - Client-side singleton for WordPress update integration
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Server-side `/update-check` REST API endpoint serving WordPress-compatible update information
|
||||||
|
- Client-side `PluginUpdateChecker` singleton hooking into WordPress's native update system
|
||||||
|
- Hooks: `pre_set_site_transient_update_plugins`, `plugins_api`, `http_request_args`
|
||||||
|
- New "Auto-Updates" settings subtab with enable/disable toggle and check frequency
|
||||||
|
- Configurable cache TTL for update checks (1-168 hours, default: 12)
|
||||||
|
- Secure download authentication via `X-License-Key` header
|
||||||
|
- Response signing support for tamper-proof update responses
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `src/Plugin.php` - Added UpdateController and PluginUpdateChecker initialization
|
||||||
|
- `src/Admin/SettingsController.php` - Added 'auto-updates' section with settings
|
||||||
|
- `openapi.json` - Documented `/update-check` endpoint with request/response schemas
|
||||||
|
- `languages/*` - Updated translations for new strings
|
||||||
|
|
||||||
|
**Settings Controller Changes:**
|
||||||
|
|
||||||
|
- Added `'auto-updates'` to `getSections()` for sub-tab navigation
|
||||||
|
- New `getAutoUpdatesSettings()` method returning enable/frequency settings
|
||||||
|
- New static methods: `isAutoUpdateEnabled()`, `getUpdateCheckFrequency()`
|
||||||
|
|
||||||
|
**UpdateController API:**
|
||||||
|
|
||||||
|
- Endpoint: `POST /wp-json/wc-licensed-product/v1/update-check`
|
||||||
|
- Request: `license_key`, `domain`, `plugin_slug` (optional), `current_version` (optional)
|
||||||
|
- Response: `update_available`, `version`, `download_url`, `package`, `changelog`, `tested`, `requires`, `requires_php`, etc.
|
||||||
|
- License validation before serving update info
|
||||||
|
- Secure download URL generation using existing DownloadController patterns
|
||||||
|
|
||||||
|
**PluginUpdateChecker Features:**
|
||||||
|
|
||||||
|
- Singleton pattern with `getInstance()`
|
||||||
|
- Caching via WordPress transients (`wclp_update_info`)
|
||||||
|
- Automatic cache clearing on settings save
|
||||||
|
- Only activates when license server URL is configured and not self-licensing
|
||||||
|
- `forceUpdateCheck()` method for manual refresh
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
To disable auto-updates programmatically:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('WC_LICENSE_DISABLE_AUTO_UPDATE', true);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Update checker only registers when `SettingsController::getPluginLicenseServerUrl()` returns a value
|
||||||
|
- Self-licensing detection prevents circular update checks (via `PluginLicenseChecker::isSelfLicensing()`)
|
||||||
|
- Download URLs include license key in `X-License-Key` header for server-side verification
|
||||||
|
- Uses Symfony HttpClient for server requests with 15s timeout
|
||||||
|
- Cache TTL configurable from 1-168 hours in settings
|
||||||
|
- OpenAPI spec updated to version 0.6.0 with full `/update-check` documentation
|
||||||
|
|
||||||
|
**Release v0.6.0:**
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|||||||
@@ -37,6 +37,80 @@
|
|||||||
color: #383d41;
|
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 */
|
/* License Packages */
|
||||||
.woocommerce-licenses {
|
.woocommerce-licenses {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
4
composer.lock
generated
4
composer.lock
generated
@@ -12,7 +12,7 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
|
||||||
"reference": "5e4b5a970f75d0163c5496581d963a24ade4f276"
|
"reference": "760e1e752a0c088fa634cf7ff678e0735ed525a4"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
|
||||||
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
|
"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",
|
"name": "psr/cache",
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
releases/wc-licensed-product-0.6.0.zip.sha256
Normal file
1
releases/wc-licensed-product-0.6.0.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
171c8195c586b3b20bac4a806e2d698cdaaf15966e2fd6e1670ec39dac8ab027 releases/wc-licensed-product-0.6.0.zip
|
||||||
1
releases/wc-licensed-product-0.6.1.zip.sha256
Normal file
1
releases/wc-licensed-product-0.6.1.zip.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
f1f1cbdfdd6cda7b20cbd2b88ab4697cde38d987e04cda1f52e885d7818d32f5 wc-licensed-product-0.6.1.zip
|
||||||
@@ -379,6 +379,19 @@ final class AdminController
|
|||||||
// Validate the license using LicenseManager
|
// Validate the license using LicenseManager
|
||||||
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
|
$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);
|
wp_send_json_success($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1605,12 +1618,11 @@ final class AdminController
|
|||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
html = '<div class="notice notice-success inline"><p><strong>✓ <?php echo esc_js(__('License is VALID', 'wc-licensed-product')); ?></strong></p></div>';
|
html = '<div class="notice notice-success inline"><p><strong>✓ <?php echo esc_js(__('License is VALID', 'wc-licensed-product')); ?></strong></p></div>';
|
||||||
html += '<table class="widefat striped"><tbody>';
|
html += '<table class="widefat striped"><tbody>';
|
||||||
html += '<tr><th><?php echo esc_js(__('Product', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
|
html += '<tr><th><?php echo esc_js(__('Product', 'wc-licensed-product')); ?></th><td><strong>' + escapeHtml(result.product_name || '-') + '</strong></td></tr>';
|
||||||
html += '<tr><th><?php echo esc_js(__('Version', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
|
|
||||||
if (result.expires_at) {
|
if (result.expires_at) {
|
||||||
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
|
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td><?php echo esc_js(__('Lifetime', 'wc-licensed-product')); ?></td></tr>';
|
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td><span class="license-lifetime"><?php echo esc_js(__('Lifetime', 'wc-licensed-product')); ?></span></td></tr>';
|
||||||
}
|
}
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ final class SettingsController
|
|||||||
*/
|
*/
|
||||||
private function getAutoUpdatesSettings(): array
|
private function getAutoUpdatesSettings(): array
|
||||||
{
|
{
|
||||||
|
$autoInstallDisabled = !self::isUpdateNotificationEnabled();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'auto_update_section_title' => [
|
'auto_update_section_title' => [
|
||||||
'name' => __('Auto-Updates', 'wc-licensed-product'),
|
'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'),
|
'desc' => __('Configure automatic plugin updates from the license server.', 'wc-licensed-product'),
|
||||||
'id' => 'wc_licensed_product_section_auto_update',
|
'id' => 'wc_licensed_product_section_auto_update',
|
||||||
],
|
],
|
||||||
'plugin_auto_update_enabled' => [
|
'update_notification_enabled' => [
|
||||||
'name' => __('Enable Auto-Updates', 'wc-licensed-product'),
|
'name' => __('Enable Update Notifications', 'wc-licensed-product'),
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'desc' => __('Automatically check for and receive plugin updates from the license server.', 'wc-licensed-product'),
|
'desc' => __('Check for and display available updates from the license server in WordPress admin.', 'wc-licensed-product'),
|
||||||
'id' => 'wc_licensed_product_plugin_auto_update_enabled',
|
'id' => 'wc_licensed_product_update_notification_enabled',
|
||||||
'default' => 'yes',
|
'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' => [
|
'update_check_frequency' => [
|
||||||
'name' => __('Check Frequency (Hours)', 'wc-licensed-product'),
|
'name' => __('Check Frequency (Hours)', 'wc-licensed-product'),
|
||||||
'type' => 'number',
|
'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
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -106,23 +106,104 @@ final class AccountController
|
|||||||
return;
|
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);
|
$licenses = $this->licenseManager->getLicensesByCustomer($customerId);
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
$filteredLicenses = $this->applyLicenseFilters($licenses, $filterProductId, $filterDomain);
|
||||||
|
|
||||||
// Group licenses by product+order into "packages"
|
// 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 {
|
try {
|
||||||
echo $this->twig->render('frontend/licenses.html.twig', [
|
echo $this->twig->render('frontend/licenses.html.twig', [
|
||||||
'packages' => $packages,
|
'packages' => $packages,
|
||||||
'has_packages' => !empty($packages),
|
'has_packages' => !empty($packages),
|
||||||
'signing_enabled' => ResponseSigner::isSigningEnabled(),
|
'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) {
|
} catch (\Exception $e) {
|
||||||
// Fallback to PHP template if Twig fails
|
// 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
|
* Group licenses into packages by product+order
|
||||||
*
|
*
|
||||||
@@ -217,10 +298,67 @@ final class AccountController
|
|||||||
/**
|
/**
|
||||||
* Fallback display method if Twig is unavailable
|
* 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'])) {
|
||||||
|
?>
|
||||||
|
<div class="wclp-filter-form">
|
||||||
|
<form method="get" action="<?php echo esc_url($licensesUrl); ?>">
|
||||||
|
<div class="wclp-filter-row">
|
||||||
|
<?php if (!empty($filterOptions['products'])): ?>
|
||||||
|
<div class="wclp-filter-field">
|
||||||
|
<label for="filter_product"><?php esc_html_e('Product', 'wc-licensed-product'); ?></label>
|
||||||
|
<select name="filter_product" id="filter_product">
|
||||||
|
<option value=""><?php esc_html_e('All Products', 'wc-licensed-product'); ?></option>
|
||||||
|
<?php foreach ($filterOptions['products'] as $id => $name): ?>
|
||||||
|
<option value="<?php echo esc_attr($id); ?>" <?php selected($currentFilterProduct, $id); ?>>
|
||||||
|
<?php echo esc_html($name); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($filterOptions['domains'])): ?>
|
||||||
|
<div class="wclp-filter-field">
|
||||||
|
<label for="filter_domain"><?php esc_html_e('Domain', 'wc-licensed-product'); ?></label>
|
||||||
|
<select name="filter_domain" id="filter_domain">
|
||||||
|
<option value=""><?php esc_html_e('All Domains', 'wc-licensed-product'); ?></option>
|
||||||
|
<?php foreach ($filterOptions['domains'] as $domain): ?>
|
||||||
|
<option value="<?php echo esc_attr($domain); ?>" <?php selected($currentFilterDomain, $domain); ?>>
|
||||||
|
<?php echo esc_html($domain); ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="wclp-filter-actions">
|
||||||
|
<button type="submit" class="button"><?php esc_html_e('Filter', 'wc-licensed-product'); ?></button>
|
||||||
|
<?php if ($isFiltered): ?>
|
||||||
|
<a href="<?php echo esc_url($licensesUrl); ?>" class="button"><?php esc_html_e('Clear', 'wc-licensed-product'); ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($packages)) {
|
if (empty($packages)) {
|
||||||
echo '<p>' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '</p>';
|
if ($isFiltered) {
|
||||||
|
echo '<p>' . esc_html__('No licenses found matching your filters.', 'wc-licensed-product') . '</p>';
|
||||||
|
} else {
|
||||||
|
echo '<p>' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '</p>';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Jeremias\WcLicensedProduct\Update;
|
namespace Jeremias\WcLicensedProduct\Update;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
|
||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
@@ -74,8 +75,8 @@ final class PluginUpdateChecker
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
// Skip if auto-updates are disabled
|
// Skip if update notifications are disabled
|
||||||
if ($this->isAutoUpdateDisabled()) {
|
if ($this->isUpdateNotificationDisabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,15 +89,19 @@ final class PluginUpdateChecker
|
|||||||
// Add authentication headers to download requests
|
// Add authentication headers to download requests
|
||||||
add_filter('http_request_args', [$this, 'addAuthHeaders'], 10, 2);
|
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
|
// 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_key', [$this, 'clearCache']);
|
||||||
add_action('update_option_wc_licensed_product_plugin_license_server_url', [$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
|
// Check constant
|
||||||
if (defined('WC_LICENSE_DISABLE_AUTO_UPDATE') && WC_LICENSE_DISABLE_AUTO_UPDATE) {
|
if (defined('WC_LICENSE_DISABLE_AUTO_UPDATE') && WC_LICENSE_DISABLE_AUTO_UPDATE) {
|
||||||
@@ -104,8 +109,25 @@ final class PluginUpdateChecker
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check setting
|
// Check setting
|
||||||
$enabled = get_option('wc_licensed_product_plugin_auto_update_enabled', 'yes');
|
return !SettingsController::isUpdateNotificationEnabled();
|
||||||
return $enabled !== 'yes';
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -424,12 +424,11 @@
|
|||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
html = '<div class="notice notice-success inline"><p><strong>✓ {{ __('License is VALID') }}</strong></p></div>';
|
html = '<div class="notice notice-success inline"><p><strong>✓ {{ __('License is VALID') }}</strong></p></div>';
|
||||||
html += '<table class="widefat striped"><tbody>';
|
html += '<table class="widefat striped"><tbody>';
|
||||||
html += '<tr><th>{{ __('Product') }}</th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
|
html += '<tr><th>{{ __('Product') }}</th><td><strong>' + escapeHtml(result.product_name || '-') + '</strong></td></tr>';
|
||||||
html += '<tr><th>{{ __('Version') }}</th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
|
|
||||||
if (result.expires_at) {
|
if (result.expires_at) {
|
||||||
html += '<tr><th>{{ __('Expires') }}</th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
|
html += '<tr><th>{{ __('Expires') }}</th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
html += '<tr><th>{{ __('Expires') }}</th><td>{{ __('Lifetime') }}</td></tr>';
|
html += '<tr><th>{{ __('Expires') }}</th><td><span class="license-lifetime">{{ __('Lifetime') }}</span></td></tr>';
|
||||||
}
|
}
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table>';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
<div class="wclp-filter-form">
|
||||||
|
<form method="get" action="{{ esc_url(licenses_url) }}">
|
||||||
|
<div class="wclp-filter-row">
|
||||||
|
{% if filter_products is defined and filter_products|length > 0 %}
|
||||||
|
<div class="wclp-filter-field">
|
||||||
|
<label for="filter_product">{{ __('Product') }}</label>
|
||||||
|
<select name="filter_product" id="filter_product">
|
||||||
|
<option value="">{{ __('All Products') }}</option>
|
||||||
|
{% for id, name in filter_products %}
|
||||||
|
<option value="{{ id }}" {{ current_filter_product == id ? 'selected' : '' }}>
|
||||||
|
{{ esc_html(name) }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if filter_domains is defined and filter_domains|length > 0 %}
|
||||||
|
<div class="wclp-filter-field">
|
||||||
|
<label for="filter_domain">{{ __('Domain') }}</label>
|
||||||
|
<select name="filter_domain" id="filter_domain">
|
||||||
|
<option value="">{{ __('All Domains') }}</option>
|
||||||
|
{% for domain in filter_domains %}
|
||||||
|
<option value="{{ esc_attr(domain) }}" {{ current_filter_domain == domain ? 'selected' : '' }}>
|
||||||
|
{{ esc_html(domain) }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="wclp-filter-actions">
|
||||||
|
<button type="submit" class="button">{{ __('Filter') }}</button>
|
||||||
|
{% if is_filtered %}
|
||||||
|
<a href="{{ esc_url(licenses_url) }}" class="button">{{ __('Clear') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if not has_packages %}
|
{% if not has_packages %}
|
||||||
<p>{{ __('You have no licenses yet.') }}</p>
|
{% if is_filtered %}
|
||||||
|
<p>{{ __('No licenses found matching your filters.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ __('You have no licenses yet.') }}</p>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="woocommerce-licenses">
|
<div class="woocommerce-licenses">
|
||||||
{% for package in packages %}
|
{% for package in packages %}
|
||||||
|
|||||||
@@ -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.6.0
|
* Version: 0.6.1
|
||||||
* 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.6.0');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.6.1');
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user