12 Commits

Author SHA1 Message Date
12a3a37658 Add product version display on single product page (v0.3.4)
- Display current version under product title for licensed products
- Add frontend CSS styling for version badge
- Update translations for new "Version:" string
- Bump version to 0.3.4

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:01:11 +01:00
b1fe34adfd Add v0.3.3 release package
Package: wc-licensed-product-0.3.3.zip (795 KB)
SHA256: a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 11:46:33 +01:00
dcf3a03598 Update translations for v0.3.3
Added German translations for license test feature:
- Test license against API
- License Validation Test modal strings
- Valid/Invalid status messages
- Error code and message labels

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 11:44:43 +01:00
38a9f0d90f Add Test and Transfer actions to PHP fallback template
The PHP fallback template (used when Twig fails) was missing the Test
license action and Transfer modal that were present in the Twig template.

- Added Test license link to row actions in PHP fallback
- Added Transfer link to row actions in PHP fallback
- Added Test License modal with AJAX validation
- Added Transfer License modal
- Added JavaScript handlers for both modals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 11:41:05 +01:00
8b87c954eb Add license test action to admin overview
Added a "Test" action button in the license overview that validates
licenses against the /validate REST API endpoint. Results are shown
in a modal with validation status, error codes, and license details.

- Added Test link in row actions for each license
- Created AJAX handler handleAjaxTestLicense() in AdminController
- Added test result modal with loading state and result display
- Shows valid/invalid status with detailed error information

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 11:37:06 +01:00
1bc643408e Fix version deactivation button not working (v0.3.3)
The toggle version button in the admin product versions table was not
deactivating versions due to incorrect parameter order in the
updateVersion() call. The isActive value was being passed to the
attachmentId parameter position instead.

- Fixed parameter order: updateVersion($id, null, !$active, null)
- Bumped version to 0.3.3
- Updated CHANGELOG.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:39:26 +01:00
875c8dd1c1 Update CLAUDE.md with v0.3.2 release information
- Added release package details and SHA256 checksum
- Documented composer.json change to use git repository URL
- Noted vendor .git directory exclusion in release packaging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:22:27 +01:00
5834e067f4 Change license client to use git repository instead of local path
- Updated composer.json repository from local path to git URL
- Package magdev/wc-licensed-product-client now fetched from:
  https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git
- Fixes symlink issues in release packages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:19:44 +01:00
79417e4971 Update translations for v0.3.2
- Regenerated POT template with updated version
- Updated German (de_CH) translation
- Compiled .mo file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:14:24 +01:00
304eb16e2e Update README with response signing documentation
- Added Response Signing section explaining X-License-Signature and X-License-Timestamp headers
- Added wp-config.php configuration example for WC_LICENSE_SERVER_SECRET
- Updated client section to recommend official magdev/wc-licensed-product-client Composer package
- Documented LicenseClient and SecureLicenseClient classes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:12:58 +01:00
df4cfc7e84 Update OpenAPI specification for v0.3.2
- Updated OpenAPI version from 0.0.7 to 0.3.2
- Added documentation for response signing headers (X-License-Signature, X-License-Timestamp)
- Enhanced API description with security information about signature verification
- Added header component definitions to OpenAPI spec
- All endpoint 200 responses now reference optional signature headers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:11:11 +01:00
812beb2a02 Update CLAUDE.md with v0.3.1 release information
- Added release package details for v0.3.1
- SHA256: 55468275522590cd68924bdf97cfcba8aa9e6ba11e2111d0234e16a1936b8adf

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 19:04:10 +01:00
17 changed files with 1454 additions and 753 deletions

View File

@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.3.4] - 2026-01-23
### Added
- Current version display on single product pages for licensed products
- Version number shown directly under the product title
- Frontend CSS styling for version badge with monospace font
### Technical Details
- Added `displayCurrentVersion()` method to `LicensedProductType` class
- Hooked to `woocommerce_single_product_summary` at priority 6 (after title)
- Added `enqueueFrontendStyles()` to load CSS on product pages
- Uses `LicensedProduct::get_current_version()` to fetch latest version
## [0.3.3] - 2026-01-22
### Fixed
- Fixed version deactivation button not working in admin product versions table
- Corrected parameter order in `updateVersion()` call - `isActive` was being passed to `attachmentId` parameter
### Technical Details
- Bug in `VersionAdminController::ajaxToggleVersion()` - parameters were in wrong order
- Changed from `updateVersion($versionId, null, null, !$currentlyActive)` to `updateVersion($versionId, null, !$currentlyActive, null)`
## [0.3.2] - 2026-01-22
### Changed
- Updated OpenAPI specification to version 0.3.2
- Added documentation for response signing headers (X-License-Signature, X-License-Timestamp)
- Enhanced API description with response signing security information
### Technical Details
- OpenAPI spec now documents optional response signature headers
- Added header component definitions for X-License-Signature and X-License-Timestamp
- All endpoint 200 responses now reference signature headers
- Improved API documentation describing SecureLicenseClient usage
## [0.3.1] - 2026-01-22
### Changed
@@ -410,7 +452,9 @@ define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
- WordPress REST API integration
- Custom WooCommerce product type extending WC_Product
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.1...HEAD
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.3...HEAD
[0.3.3]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.2...v0.3.3
[0.3.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.1...v0.3.2
[0.3.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.3.0...v0.3.1
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.2...v0.3.0
[0.2.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.2.1...v0.2.2

View File

@@ -840,3 +840,95 @@ Reorganized the settings page with WooCommerce-style sub-tab navigation for bett
- Added `outputSections()` for WooCommerce-style navigation rendering
- Split `getSettingsFields()` into section-specific methods using PHP 8 match expression
- Hooks: `woocommerce_sections_licensed_product` for sub-navigation
**Release v0.3.1:**
- Created release package: `releases/wc-licensed-product-0.3.1.zip` (754 KB)
- SHA256: `55468275522590cd68924bdf97cfcba8aa9e6ba11e2111d0234e16a1936b8adf`
- Tagged as `v0.3.1` and pushed to `main` branch
### 2026-01-22 - Version 0.3.2 - OpenAPI Update
**Overview:**
Updated OpenAPI specification to document response signing feature added in v0.2.0.
**Implemented:**
- Updated OpenAPI version from 0.0.7 to 0.3.2
- Added documentation for X-License-Signature and X-License-Timestamp headers
- Enhanced API description with response signing security information
- Added header component definitions in OpenAPI spec
**Modified files:**
- `openapi.json` - Updated version and added signature header documentation
**Technical notes:**
- All endpoint 200 responses now reference optional signature headers
- Header definitions added to components section
- API description explains SecureLicenseClient usage for signature verification
- Changed `magdev/wc-licensed-product-client` from local path to git repository URL
- Composer now fetches from: `https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git`
- Release package excludes vendor `.git` directories
**Release v0.3.2:**
- Created release package: `releases/wc-licensed-product-0.3.2.zip` (810 KB)
- SHA256: `ca33c81516b5dcf4a80b3192d8ae4ad39a7bf67196a1f729b563c5ae01b1d39c`
- Tagged as `v0.3.2` and pushed to `main` branch
### 2026-01-22 - Version 0.3.3 - Bug Fix & License Testing
**Overview:**
Fixed version deactivation bug and added license testing functionality.
**Bug Fix:**
- Fixed version deactivation button not working in admin product versions table
- Root cause: Parameters in wrong order in `VersionAdminController::ajaxToggleVersion()`
- Changed from `updateVersion($versionId, null, null, !$currentlyActive)` to `updateVersion($versionId, null, !$currentlyActive, null)`
**Implemented:**
- Added "Test" action to license overview to validate licenses against `/validate` API endpoint
- Test License modal showing license key, domain, and validation results
- AJAX handler `handleAjaxTestLicense()` for license testing
**Modified files:**
- `src/Admin/VersionAdminController.php` - Fixed parameter order in toggle method
- `src/Admin/AdminController.php` - Added Test action to PHP fallback and AJAX handler
- `templates/admin/licenses.html.twig` - Added Test action and modal to Twig template
**Release v0.3.3:**
- Created release package: `releases/wc-licensed-product-0.3.3.zip` (795 KB)
- SHA256: `a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c`
- Tagged as `v0.3.3` and pushed to `main` branch
### 2026-01-23 - Version 0.3.4 - Frontend Version Display
**Overview:**
Added current version display on single product pages for licensed products.
**Implemented:**
- Current version displayed directly under the product title
- Styled version badge with monospace font and subtle blue background
- Frontend CSS automatically loaded on licensed product pages
**Modified files:**
- `src/Product/LicensedProductType.php` - Added `displayCurrentVersion()` and `enqueueFrontendStyles()` methods
- `assets/css/frontend.css` - Added `.wclp-product-version` styles
**Technical notes:**
- Uses `woocommerce_single_product_summary` hook at priority 6 (after title at priority 5)
- Only displays for licensed product type
- Only displays if product has at least one version defined
- Uses `LicensedProduct::get_current_version()` which queries `VersionManager::getLatestVersion()`

View File

@@ -107,12 +107,42 @@ When a customer purchases a licensed product, they must enter the domain where t
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
### Client Examples
### Response Signing (Optional)
Ready-to-use API client examples are available in `docs/client-examples/`:
When the server is configured with a shared secret, all API responses include cryptographic signatures for tamper protection:
**Configuration (wp-config.php):**
```php
define('WC_LICENSE_SERVER_SECRET', 'your-secure-random-string-min-32-chars');
```
**Response Headers:**
| Header | Description |
| ------ | ----------- |
| `X-License-Signature` | HMAC-SHA256 signature of the response body |
| `X-License-Timestamp` | Unix timestamp when the response was generated |
The signature prevents man-in-the-middle attacks and ensures response integrity. Use the `magdev/wc-licensed-product-client` Composer package with the `SecureLicenseClient` class to automatically verify signatures.
### Client Libraries & Examples
**PHP (Recommended):** Install the official client library via Composer:
```bash
composer require magdev/wc-licensed-product-client
```
The library provides:
- `LicenseClient` - Standard client for API calls
- `SecureLicenseClient` - Client with automatic response signature verification
**Example clients** for other languages are available in `docs/client-examples/`:
- **cURL** - Shell script examples ([curl.sh](docs/client-examples/curl.sh))
- **PHP** - Client class with examples ([php-client.php](docs/client-examples/php-client.php))
- **PHP** - Standalone client example ([php-client.php](docs/client-examples/php-client.php))
- **Python** - Client class with dataclasses ([python-client.py](docs/client-examples/python-client.py))
- **JavaScript** - Browser and Node.js client ([javascript-client.js](docs/client-examples/javascript-client.js))
- **C#** - Async client with System.Text.Json ([csharp-client.cs](docs/client-examples/csharp-client.cs))

View File

@@ -528,3 +528,24 @@
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Product Version Display (Single Product Page) */
.wclp-product-version {
margin: 0.5em 0 1em 0;
font-size: 0.95em;
color: #666;
}
.wclp-product-version .version-label {
font-weight: 500;
color: #555;
}
.wclp-product-version .version-number {
font-family: 'SF Mono', Monaco, Consolas, monospace;
background: #e7f3ff;
padding: 0.15em 0.5em;
border-radius: 3px;
color: #2271b1;
font-weight: 500;
}

View File

@@ -12,8 +12,8 @@
],
"repositories": [
{
"type": "path",
"url": "/home/magdev/workspaces/php/wc-licensed-product-client"
"type": "vcs",
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git"
}
],
"require": {

15
composer.lock generated
View File

@@ -4,15 +4,15 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0387e179142771dbc12a8dba42895bd0",
"content-hash": "05af8ab515abe7e689c610724b54e27a",
"packages": [
{
"name": "magdev/wc-licensed-product-client",
"version": "dev-main",
"dist": {
"type": "path",
"url": "/home/magdev/workspaces/php/wc-licensed-product-client",
"reference": "83037ea0c2d9e365cf9ec0ad50251d3ebc7e4782"
"source": {
"type": "git",
"url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
"reference": "a3a957914fd6ef74cb479e213d1d3bc0606f496b"
},
"require": {
"php": "^8.3",
@@ -24,6 +24,7 @@
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
@@ -51,9 +52,7 @@
"issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
"source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
},
"transport-options": {
"relative": false
}
"time": "2026-01-22T20:05:48+00:00"
},
{
"name": "psr/cache",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,8 @@
"openapi": "3.1.0",
"info": {
"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.",
"version": "0.0.7",
"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",
"contact": {
"name": "Marco Graetsch",
"url": "https://src.bundespruefstelle.ch/magdev",
@@ -55,6 +55,14 @@
"responses": {
"200": {
"description": "License is valid for the specified domain",
"headers": {
"X-License-Signature": {
"$ref": "#/components/headers/X-License-Signature"
},
"X-License-Timestamp": {
"$ref": "#/components/headers/X-License-Timestamp"
}
},
"content": {
"application/json": {
"schema": {
@@ -156,6 +164,14 @@
"responses": {
"200": {
"description": "License status retrieved successfully",
"headers": {
"X-License-Signature": {
"$ref": "#/components/headers/X-License-Signature"
},
"X-License-Timestamp": {
"$ref": "#/components/headers/X-License-Timestamp"
}
},
"content": {
"application/json": {
"schema": {
@@ -221,6 +237,14 @@
"responses": {
"200": {
"description": "License activated successfully or already activated",
"headers": {
"X-License-Signature": {
"$ref": "#/components/headers/X-License-Signature"
},
"X-License-Timestamp": {
"$ref": "#/components/headers/X-License-Timestamp"
}
},
"content": {
"application/json": {
"schema": {
@@ -519,6 +543,26 @@
}
}
}
},
"headers": {
"X-License-Signature": {
"description": "HMAC-SHA256 signature of the response body for tamper protection. Only present when server is configured with WC_LICENSE_SERVER_SECRET. Signature format: hex-encoded HMAC-SHA256 of (timestamp + ':' + canonical_json_body) using a per-license derived key.",
"schema": {
"type": "string",
"pattern": "^[a-f0-9]{64}$",
"example": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
},
"required": false
},
"X-License-Timestamp": {
"description": "Unix timestamp when the response was generated. Used together with X-License-Signature to prevent replay attacks. Only present when server is configured with WC_LICENSE_SERVER_SECRET.",
"schema": {
"type": "string",
"pattern": "^[0-9]+$",
"example": "1737550000"
},
"required": false
}
}
},
"tags": [

View File

@@ -0,0 +1 @@
a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c wc-licensed-product-0.3.3.zip

Binary file not shown.

View File

@@ -61,6 +61,9 @@ final class AdminController
add_action('wp_ajax_wclp_update_license_expiry', [$this, 'handleAjaxExpiryUpdate']);
add_action('wp_ajax_wclp_update_license_domain', [$this, 'handleAjaxDomainUpdate']);
add_action('wp_ajax_wclp_revoke_license', [$this, 'handleAjaxRevoke']);
// AJAX handler for license testing
add_action('wp_ajax_wclp_test_license', [$this, 'handleAjaxTestLicense']);
}
/**
@@ -355,6 +358,30 @@ final class AdminController
}
}
/**
* Handle AJAX license test - validates license against the API
*/
public function handleAjaxTestLicense(): void
{
check_ajax_referer('wclp_inline_edit', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$licenseKey = isset($_POST['license_key']) ? sanitize_text_field(wp_unslash($_POST['license_key'])) : '';
$domain = isset($_POST['domain']) ? sanitize_text_field(wp_unslash($_POST['domain'])) : '';
if (empty($licenseKey) || empty($domain)) {
wp_send_json_error(['message' => __('License key and domain are required.', 'wc-licensed-product')]);
}
// Validate the license using LicenseManager
$result = $this->licenseManager->validateLicense($licenseKey, $domain);
wp_send_json_success($result);
}
/**
* Handle admin actions (update, delete licenses)
*/
@@ -1347,7 +1374,20 @@ final class AdminController
</td>
<td class="license-actions">
<div class="row-actions">
<span class="test">
<a href="#" class="wclp-test-license-link"
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>"
data-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
title="<?php esc_attr_e('Test license against API', 'wc-licensed-product'); ?>"><?php esc_html_e('Test', 'wc-licensed-product'); ?></a> |
</span>
<?php if ($item['license']->getStatus() !== License::STATUS_REVOKED): ?>
<span class="transfer">
<a href="#" class="wclp-transfer-link"
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
data-current-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
title="<?php esc_attr_e('Transfer to new domain', 'wc-licensed-product'); ?>"><?php esc_html_e('Transfer', 'wc-licensed-product'); ?></a> |
</span>
<span class="extend">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $item['license']->getId() . '&days=30'),
@@ -1429,8 +1469,69 @@ final class AdminController
</div>
</form>
<!-- Test License Modal -->
<div id="wclp-test-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</span>
<h2><?php esc_html_e('License Validation Test', 'wc-licensed-product'); ?></h2>
<div class="wclp-test-info">
<table class="form-table">
<tr>
<th scope="row"><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<td><code id="test-license-key"></code></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<td><code id="test-domain"></code></td>
</tr>
</table>
</div>
<div id="wclp-test-loading" style="display:none; text-align:center; padding:20px;">
<span class="spinner is-active" style="float:none;"></span>
<p><?php esc_html_e('Testing license...', 'wc-licensed-product'); ?></p>
</div>
<div id="wclp-test-result" style="display:none;">
<div id="wclp-test-result-content"></div>
</div>
<p class="submit">
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Close', 'wc-licensed-product'); ?></button>
</p>
</div>
</div>
<!-- Transfer Modal -->
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</span>
<h2><?php esc_html_e('Transfer License to New Domain', 'wc-licensed-product'); ?></h2>
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>">
<input type="hidden" name="action" value="transfer_license">
<?php wp_nonce_field('transfer_license', '_wpnonce'); ?>
<input type="hidden" name="license_id" id="transfer-license-id" value="">
<table class="form-table">
<tr>
<th scope="row"><label><?php esc_html_e('Current Domain', 'wc-licensed-product'); ?></label></th>
<td><code id="transfer-current-domain"></code></td>
</tr>
<tr>
<th scope="row"><label for="new_domain"><?php esc_html_e('New Domain', 'wc-licensed-product'); ?></label></th>
<td>
<input type="text" name="new_domain" id="transfer-new-domain" class="regular-text" placeholder="example.com" required>
<p class="description"><?php esc_html_e('Enter the new domain without http:// or www.', 'wc-licensed-product'); ?></p>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Transfer License', 'wc-licensed-product'); ?></button>
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</p>
</form>
</div>
</div>
<script>
(function($) {
// Checkbox select all
$('#cb-select-all-1, #cb-select-all-2').on('change', function() {
$('input[name="license_ids[]"]').prop('checked', this.checked);
$('#cb-select-all-1, #cb-select-all-2').prop('checked', this.checked);
@@ -1445,6 +1546,102 @@ final class AdminController
$('#bulk-action-selector').val(bottomAction);
}
});
// Transfer modal
var $transferModal = $('#wclp-transfer-modal');
$('.wclp-transfer-link').on('click', function(e) {
e.preventDefault();
var licenseId = $(this).data('license-id');
var currentDomain = $(this).data('current-domain');
$('#transfer-license-id').val(licenseId);
$('#transfer-current-domain').text(currentDomain);
$('#transfer-new-domain').val('');
$transferModal.show();
});
// Test License modal
var $testModal = $('#wclp-test-modal');
var $testLoading = $('#wclp-test-loading');
var $testResult = $('#wclp-test-result');
var $testResultContent = $('#wclp-test-result-content');
$('.wclp-test-license-link').on('click', function(e) {
e.preventDefault();
var licenseKey = $(this).data('license-key');
var domain = $(this).data('domain');
$('#test-license-key').text(licenseKey);
$('#test-domain').text(domain);
$testLoading.show();
$testResult.hide();
$testModal.show();
$.ajax({
url: wclpAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wclp_test_license',
nonce: wclpAdmin.editNonce,
license_key: licenseKey,
domain: domain
},
success: function(response) {
$testLoading.hide();
if (response.success) {
var result = response.data;
var html = '';
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 += '<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(__('Version', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
if (result.expires_at) {
html += '<tr><th><?php echo esc_js(__('Expires', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
} 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 += '</tbody></table>';
} else {
html = '<div class="notice notice-error inline"><p><strong>✗ <?php echo esc_js(__('License is INVALID', 'wc-licensed-product')); ?></strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th><?php echo esc_js(__('Error Code', 'wc-licensed-product')); ?></th><td><code>' + escapeHtml(result.error || 'unknown') + '</code></td></tr>';
html += '<tr><th><?php echo esc_js(__('Message', 'wc-licensed-product')); ?></th><td>' + escapeHtml(result.message || '-') + '</td></tr>';
html += '</tbody></table>';
}
$testResultContent.html(html);
$testResult.show();
} else {
$testResultContent.html('<div class="notice notice-error inline"><p>' + escapeHtml(response.data.message || 'Error') + '</p></div>');
$testResult.show();
}
},
error: function() {
$testLoading.hide();
$testResultContent.html('<div class="notice notice-error inline"><p><?php echo esc_js(__('Failed to test license. Please try again.', 'wc-licensed-product')); ?></p></div>');
$testResult.show();
}
});
});
// Close modals
$('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
$(this).closest('.wclp-modal').hide();
});
$(window).on('click', function(e) {
if ($(e.target).hasClass('wclp-modal')) {
$(e.target).hide();
}
});
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})(jQuery);
</script>
</div>

View File

@@ -361,7 +361,7 @@ final class VersionAdminController
wp_send_json_error(['message' => __('Version ID is required.', 'wc-licensed-product')]);
}
$result = $this->versionManager->updateVersion($versionId, null, null, !$currentlyActive);
$result = $this->versionManager->updateVersion($versionId, null, !$currentlyActive, null);
if (!$result) {
wp_send_json_error(['message' => __('Failed to update version.', 'wc-licensed-product')]);

View File

@@ -45,6 +45,12 @@ final class LicensedProductType
// Make product virtual by default
add_filter('woocommerce_product_is_virtual', [$this, 'isVirtual'], 10, 2);
// Display current version under product title on single product page
add_action('woocommerce_single_product_summary', [$this, 'displayCurrentVersion'], 6);
// Enqueue frontend CSS for licensed products on single product pages
add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
}
/**
@@ -235,4 +241,52 @@ final class LicensedProductType
}
return $isVirtual;
}
/**
* Enqueue frontend styles for licensed products on single product pages
*/
public function enqueueFrontendStyles(): void
{
if (!is_product()) {
return;
}
global $product;
if (!$product || !$product->is_type('licensed')) {
return;
}
wp_enqueue_style(
'wc-licensed-product-frontend',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/frontend.css',
[],
WC_LICENSED_PRODUCT_VERSION
);
}
/**
* Display current version under product title on single product page
*/
public function displayCurrentVersion(): void
{
global $product;
if (!$product || !$product->is_type('licensed')) {
return;
}
/** @var LicensedProduct $product */
$version = $product->get_current_version();
if (empty($version)) {
return;
}
printf(
'<p class="wclp-product-version"><span class="version-label">%s</span> <span class="version-number">%s</span></p>',
esc_html__('Version:', 'wc-licensed-product'),
esc_html($version)
);
}
}

View File

@@ -184,6 +184,13 @@
</td>
<td class="license-actions">
<div class="row-actions">
<span class="test">
<a href="#" class="wclp-test-license-link"
data-license-id="{{ item.license.id }}"
data-license-key="{{ esc_attr(item.license.licenseKey) }}"
data-domain="{{ esc_attr(item.license.domain) }}"
title="{{ __('Test license against API') }}">{{ __('Test') }}</a> |
</span>
{% if item.license.status != 'revoked' %}
<span class="transfer">
<a href="#" class="wclp-transfer-link"
@@ -272,6 +279,36 @@
</form>
</div>
<!-- Test License Modal -->
<div id="wclp-test-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</span>
<h2>{{ __('License Validation Test') }}</h2>
<div class="wclp-test-info">
<table class="form-table">
<tr>
<th scope="row">{{ __('License Key') }}</th>
<td><code id="test-license-key"></code></td>
</tr>
<tr>
<th scope="row">{{ __('Domain') }}</th>
<td><code id="test-domain"></code></td>
</tr>
</table>
</div>
<div id="wclp-test-loading" style="display:none; text-align:center; padding:20px;">
<span class="spinner is-active" style="float:none;"></span>
<p>{{ __('Testing license...') }}</p>
</div>
<div id="wclp-test-result" style="display:none;">
<div id="wclp-test-result-content"></div>
</div>
<p class="submit">
<button type="button" class="button wclp-modal-cancel">{{ __('Close') }}</button>
</p>
</div>
</div>
<!-- Transfer Modal -->
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
@@ -349,5 +386,91 @@
$modal.hide();
}
});
// Test License modal
var $testModal = $('#wclp-test-modal');
var $testLoading = $('#wclp-test-loading');
var $testResult = $('#wclp-test-result');
var $testResultContent = $('#wclp-test-result-content');
$('.wclp-test-license-link').on('click', function(e) {
e.preventDefault();
var licenseKey = $(this).data('license-key');
var domain = $(this).data('domain');
// Show modal with info
$('#test-license-key').text(licenseKey);
$('#test-domain').text(domain);
$testLoading.show();
$testResult.hide();
$testModal.show();
// Call the test endpoint
$.ajax({
url: wclpAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wclp_test_license',
nonce: wclpAdmin.editNonce,
license_key: licenseKey,
domain: domain
},
success: function(response) {
$testLoading.hide();
if (response.success) {
var result = response.data;
var html = '';
if (result.valid) {
html = '<div class="notice notice-success inline"><p><strong>✓ {{ __('License is VALID') }}</strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th>{{ __('Product') }}</th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
html += '<tr><th>{{ __('Version') }}</th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
if (result.expires_at) {
html += '<tr><th>{{ __('Expires') }}</th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
} else {
html += '<tr><th>{{ __('Expires') }}</th><td>{{ __('Lifetime') }}</td></tr>';
}
html += '</tbody></table>';
} else {
html = '<div class="notice notice-error inline"><p><strong>✗ {{ __('License is INVALID') }}</strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th>{{ __('Error Code') }}</th><td><code>' + escapeHtml(result.error || 'unknown') + '</code></td></tr>';
html += '<tr><th>{{ __('Message') }}</th><td>' + escapeHtml(result.message || '-') + '</td></tr>';
html += '</tbody></table>';
}
$testResultContent.html(html);
$testResult.show();
} else {
$testResultContent.html('<div class="notice notice-error inline"><p>' + escapeHtml(response.data.message || 'Error') + '</p></div>');
$testResult.show();
}
},
error: function() {
$testLoading.hide();
$testResultContent.html('<div class="notice notice-error inline"><p>{{ __('Failed to test license. Please try again.') }}</p></div>');
$testResult.show();
}
});
});
// Close test modal
$testModal.find('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
$testModal.hide();
});
$(window).on('click', function(e) {
if ($(e.target).is($testModal)) {
$testModal.hide();
}
});
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})(jQuery);
</script>

View File

@@ -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.3.1
* Version: 0.3.4
* 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.3.1');
define('WC_LICENSED_PRODUCT_VERSION', '0.3.4');
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__));