You've already forked wc-licensed-product
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41e46fc7b8 | |||
| 549a58dc5d | |||
| 7d02105284 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.2] - 2026-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Per-license customer secrets for API response verification
|
||||||
|
- "API Verification Secret" section in customer account licenses page (collapsible)
|
||||||
|
- Copy button for customer secrets with clipboard support
|
||||||
|
- Documentation for per-license secret derivation and usage
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Customers no longer need the master server secret for signature verification
|
||||||
|
- Each license key has a unique derived secret using HKDF-like key derivation
|
||||||
|
- If one customer's secret is compromised, other customers remain unaffected
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated `ResponseSigner` with static methods for secret derivation
|
||||||
|
- Updated `server-implementation.md` with per-license secret documentation
|
||||||
|
- Added new translation strings for secret-related UI
|
||||||
|
|
||||||
## [0.5.1] - 2026-01-26
|
## [0.5.1] - 2026-01-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
37
CLAUDE.md
37
CLAUDE.md
@@ -32,18 +32,14 @@ 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.
|
||||||
|
|
||||||
### Known Bugs
|
### Version 0.5.2
|
||||||
|
|
||||||
No known bugs at the moment.
|
*No planned bugfixes yet.*
|
||||||
|
|
||||||
### Version 0.6.0
|
### Version 0.6.0
|
||||||
|
|
||||||
*No planned features yet.*
|
*No planned features yet.*
|
||||||
|
|
||||||
### Version 0.5.1
|
|
||||||
|
|
||||||
*No planned bugfixes yet.*
|
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
- **Language:** PHP 8.3.x
|
- **Language:** PHP 8.3.x
|
||||||
@@ -1268,3 +1264,32 @@ Major feature release enabling customers to purchase multiple licenses for diffe
|
|||||||
- Created release package: `releases/wc-licensed-product-0.5.0.zip` (863 KB)
|
- Created release package: `releases/wc-licensed-product-0.5.0.zip` (863 KB)
|
||||||
- SHA256: `446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316`
|
- SHA256: `446804948e5f99d705b548061d5b78180856984c58458640a910ada8f27f5316`
|
||||||
- Tagged as `v0.5.0` and pushed to `main` branch
|
- Tagged as `v0.5.0` and pushed to `main` branch
|
||||||
|
|
||||||
|
### 2026-01-26 - Version 0.5.1 - Admin UI Fixes
|
||||||
|
|
||||||
|
**Overview:**
|
||||||
|
|
||||||
|
Bug fix release improving admin UI usability for version management and license overview.
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- Fixed: Product versions in admin now sort by version DESC when adding via AJAX
|
||||||
|
- Fixed: License actions in admin overview are now always visible (not just on hover)
|
||||||
|
|
||||||
|
**Modified files:**
|
||||||
|
|
||||||
|
- `assets/css/admin.css` - Added `!important` to `.licenses-table .row-actions` for permanent visibility
|
||||||
|
- `assets/js/versions.js` - Added `compareVersions()` function and sorted insertion for AJAX-added versions
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Version sorting uses semantic version comparison (major.minor.patch)
|
||||||
|
- New versions are inserted in correct sorted position in the table instead of always appending
|
||||||
|
- CSS override uses `!important` to overcome WordPress default hover-only behavior for row actions
|
||||||
|
- `compareVersions()` function compares version strings numerically (1.10.0 > 1.9.0)
|
||||||
|
|
||||||
|
**Release v0.5.1:**
|
||||||
|
|
||||||
|
- Created release package: `releases/wc-licensed-product-0.5.1.zip` (863 KB)
|
||||||
|
- SHA256: `a489f0b8cfcd7d5d9b2021b7ff581b9f1a56468dfde87bbb06bb4555d11f7556`
|
||||||
|
- Tagged as `v0.5.1` and pushed to `main` branch
|
||||||
|
|||||||
@@ -863,3 +863,118 @@
|
|||||||
color: #2271b1;
|
color: #2271b1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Customer Secret Section */
|
||||||
|
.license-row-secret {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
padding-top: 0.75em;
|
||||||
|
border-top: 1px dashed #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35em;
|
||||||
|
padding: 0.4em 0.75em;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #ccc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle .toggle-arrow {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle[aria-expanded="true"] .toggle-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-content {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
padding: 1em;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-description {
|
||||||
|
margin: 0 0 0.75em 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-value-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-value {
|
||||||
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.75em;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn .dashicons {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.secret-value-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-value {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-secret-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
bindEvents: function() {
|
bindEvents: function() {
|
||||||
$(document).on('click', '.copy-license-btn', this.copyLicenseKey);
|
$(document).on('click', '.copy-license-btn', this.copyLicenseKey);
|
||||||
|
$(document).on('click', '.copy-secret-btn', this.copySecret);
|
||||||
|
|
||||||
// Transfer modal events
|
// Transfer modal events
|
||||||
$(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this));
|
$(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this));
|
||||||
@@ -28,6 +29,9 @@
|
|||||||
// Older versions toggle
|
// Older versions toggle
|
||||||
$(document).on('click', '.older-versions-toggle', this.toggleOlderVersions);
|
$(document).on('click', '.older-versions-toggle', this.toggleOlderVersions);
|
||||||
|
|
||||||
|
// Secret toggle
|
||||||
|
$(document).on('click', '.secret-toggle', this.toggleSecret);
|
||||||
|
|
||||||
// Close modal on escape key
|
// Close modal on escape key
|
||||||
$(document).on('keyup', function(e) {
|
$(document).on('keyup', function(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -50,6 +54,47 @@
|
|||||||
$list.slideToggle(200);
|
$list.slideToggle(200);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle secret visibility
|
||||||
|
*/
|
||||||
|
toggleSecret: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(this);
|
||||||
|
var $content = $btn.siblings('.secret-content');
|
||||||
|
var isExpanded = $btn.attr('aria-expanded') === 'true';
|
||||||
|
|
||||||
|
$btn.attr('aria-expanded', !isExpanded);
|
||||||
|
$content.slideToggle(200);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy secret to clipboard
|
||||||
|
*/
|
||||||
|
copySecret: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(this);
|
||||||
|
var secret = $btn.data('secret');
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use modern clipboard API if available
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(secret)
|
||||||
|
.then(function() {
|
||||||
|
WCLicensedProductFrontend.showCopyFeedback($btn, true);
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
WCLicensedProductFrontend.fallbackCopy(secret, $btn);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
WCLicensedProductFrontend.fallbackCopy(secret, $btn);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy license key to clipboard
|
* Copy license key to clipboard
|
||||||
*/
|
*/
|
||||||
|
|||||||
12
composer.lock
generated
12
composer.lock
generated
@@ -380,16 +380,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-client",
|
"name": "symfony/http-client",
|
||||||
"version": "v7.4.3",
|
"version": "v7.4.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/http-client.git",
|
"url": "https://github.com/symfony/http-client.git",
|
||||||
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616"
|
"reference": "d63c23357d74715a589454c141c843f0172bec6c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/http-client/zipball/d01dfac1e0dc99f18da48b18101c23ce57929616",
|
"url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c",
|
||||||
"reference": "d01dfac1e0dc99f18da48b18101c23ce57929616",
|
"reference": "d63c23357d74715a589454c141c843f0172bec6c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
"http"
|
"http"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/http-client/tree/v7.4.3"
|
"source": "https://github.com/symfony/http-client/tree/v7.4.4"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -477,7 +477,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-12-23T14:50:43+00:00"
|
"time": "2026-01-23T16:34:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-client-contracts",
|
"name": "symfony/http-client-contracts",
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ The security model works as follows:
|
|||||||
|
|
||||||
1. Server generates a unique signature for each response using HMAC-SHA256
|
1. Server generates a unique signature for each response using HMAC-SHA256
|
||||||
2. Signature includes a timestamp to prevent replay attacks
|
2. Signature includes a timestamp to prevent replay attacks
|
||||||
3. Client verifies the signature using a shared secret
|
3. Each license key has a unique derived secret (not the master secret)
|
||||||
4. Invalid signatures cause the client to reject the response
|
4. Client verifies the signature using their per-license secret
|
||||||
|
5. Invalid signatures cause the client to reject the response
|
||||||
|
|
||||||
This prevents attackers from:
|
This prevents attackers from:
|
||||||
|
|
||||||
- Faking valid license responses
|
- Faking valid license responses
|
||||||
- Replaying old responses
|
- Replaying old responses
|
||||||
- Tampering with response data
|
- Tampering with response data
|
||||||
|
- Using one customer's secret to verify another customer's responses
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -323,13 +325,49 @@ Adjust if needed:
|
|||||||
$signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes
|
$signature = new ResponseSignature($key, timestampTolerance: 600); // 10 minutes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Per-License Secrets
|
||||||
|
|
||||||
|
Each customer receives a unique secret derived from their license key. This means:
|
||||||
|
|
||||||
|
- Customers only know their own secret, not the master server secret
|
||||||
|
- If one customer's secret is leaked, other customers are not affected
|
||||||
|
- The server uses HKDF-like derivation to create unique secrets
|
||||||
|
|
||||||
|
#### How Customers Get Their Secret
|
||||||
|
|
||||||
|
Customers can find their per-license verification secret in their account:
|
||||||
|
|
||||||
|
1. Log in to the store
|
||||||
|
2. Go to My Account > Licenses
|
||||||
|
3. Click "API Verification Secret" under any license
|
||||||
|
4. Copy the 64-character hex string
|
||||||
|
|
||||||
|
This secret is automatically derived from the customer's license key and the server's master secret.
|
||||||
|
|
||||||
|
#### Using the Customer Secret
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
|
||||||
|
// Customer uses their per-license secret (from account page)
|
||||||
|
$client = new SecureLicenseClient(
|
||||||
|
httpClient: HttpClient::create(),
|
||||||
|
baseUrl: 'https://shop.example.com',
|
||||||
|
serverSecret: 'customer-secret-from-account-page', // 64 hex chars
|
||||||
|
);
|
||||||
|
|
||||||
|
$info = $client->validate('XXXX-XXXX-XXXX-XXXX', 'example.com');
|
||||||
|
```
|
||||||
|
|
||||||
### Secret Key Rotation
|
### Secret Key Rotation
|
||||||
|
|
||||||
To rotate the server secret:
|
To rotate the server secret:
|
||||||
|
|
||||||
1. Deploy new secret to server
|
1. Deploy new secret to server
|
||||||
2. Update client configurations
|
2. All per-license secrets change automatically (they're derived)
|
||||||
3. Old signatures become invalid immediately
|
3. Customers must copy their new secret from their account page
|
||||||
|
4. Old signatures become invalid immediately
|
||||||
|
|
||||||
For zero-downtime rotation, implement versioned secrets:
|
For zero-downtime rotation, implement versioned secrets:
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -147,9 +147,52 @@ final class ResponseSigner
|
|||||||
*/
|
*/
|
||||||
private function deriveKey(string $licenseKey): string
|
private function deriveKey(string $licenseKey): string
|
||||||
{
|
{
|
||||||
// HKDF-like key derivation
|
return self::deriveCustomerSecret($licenseKey, $this->serverSecret);
|
||||||
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
|
}
|
||||||
|
|
||||||
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
|
/**
|
||||||
|
* Derive a customer-specific secret from a license key
|
||||||
|
*
|
||||||
|
* This secret is unique per license and can be shared with the customer
|
||||||
|
* to verify signed API responses. Each customer gets their own secret
|
||||||
|
* derived from their license key.
|
||||||
|
*
|
||||||
|
* @param string $licenseKey The customer's license key
|
||||||
|
* @param string $serverSecret The server's master secret
|
||||||
|
* @return string The derived secret (64 hex characters)
|
||||||
|
*/
|
||||||
|
public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
|
||||||
|
{
|
||||||
|
// HKDF-like key derivation
|
||||||
|
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
|
||||||
|
|
||||||
|
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the customer secret for a license key using the configured server secret
|
||||||
|
*
|
||||||
|
* @param string $licenseKey The customer's license key
|
||||||
|
* @return string|null The derived secret, or null if server secret is not configured
|
||||||
|
*/
|
||||||
|
public static function getCustomerSecretForLicense(string $licenseKey): ?string
|
||||||
|
{
|
||||||
|
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : '';
|
||||||
|
|
||||||
|
if (empty($serverSecret)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::deriveCustomerSecret($licenseKey, $serverSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if response signing is enabled
|
||||||
|
*
|
||||||
|
* @return bool True if server secret is configured
|
||||||
|
*/
|
||||||
|
public static function isSigningEnabled(): bool
|
||||||
|
{
|
||||||
|
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Jeremias\WcLicensedProduct\Frontend;
|
namespace Jeremias\WcLicensedProduct\Frontend;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Api\ResponseSigner;
|
||||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||||
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
use Jeremias\WcLicensedProduct\Product\VersionManager;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
@@ -114,6 +115,7 @@ final class AccountController
|
|||||||
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(),
|
||||||
]);
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Fallback to PHP template if Twig fails
|
// Fallback to PHP template if Twig fails
|
||||||
@@ -161,6 +163,7 @@ final class AccountController
|
|||||||
'status' => $license->getStatus(),
|
'status' => $license->getStatus(),
|
||||||
'expires_at' => $license->getExpiresAt(),
|
'expires_at' => $license->getExpiresAt(),
|
||||||
'is_transferable' => in_array($license->getStatus(), ['active', 'inactive'], true),
|
'is_transferable' => in_array($license->getStatus(), ['active', 'inactive'], true),
|
||||||
|
'customer_secret' => ResponseSigner::getCustomerSecretForLicense($license->getLicenseKey()),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Track if package has at least one active license
|
// Track if package has at least one active license
|
||||||
|
|||||||
@@ -65,6 +65,26 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if signing_enabled and license.customer_secret %}
|
||||||
|
<div class="license-row-secret">
|
||||||
|
<button type="button" class="secret-toggle" aria-expanded="false">
|
||||||
|
<span class="dashicons dashicons-lock"></span>
|
||||||
|
{{ __('API Verification Secret') }}
|
||||||
|
<span class="dashicons dashicons-arrow-down-alt2 toggle-arrow"></span>
|
||||||
|
</button>
|
||||||
|
<div class="secret-content" style="display: none;">
|
||||||
|
<p class="secret-description">
|
||||||
|
{{ __('Use this secret to verify signed API responses. Keep it secure.') }}
|
||||||
|
</p>
|
||||||
|
<div class="secret-value-wrapper">
|
||||||
|
<code class="secret-value">{{ esc_html(license.customer_secret) }}</code>
|
||||||
|
<button type="button" class="copy-secret-btn" data-secret="{{ esc_attr(license.customer_secret) }}" title="{{ __('Copy to clipboard') }}">
|
||||||
|
<span class="dashicons dashicons-clipboard"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WooCommerce Licensed Product
|
* Plugin Name: WooCommerce Licensed Product
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
||||||
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||||
* Version: 0.5.1
|
* Version: 0.5.2
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL-2.0-or-later
|
* License: GPL-2.0-or-later
|
||||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Plugin constants
|
// Plugin constants
|
||||||
define('WC_LICENSED_PRODUCT_VERSION', '0.5.1');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.5.2');
|
||||||
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