You've already forked wc-licensed-product
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7967f71ab | |||
| 1de8257527 | |||
| 26245c0c57 | |||
| a6c6d247aa | |||
| fba8bf2352 | |||
| 12a3a37658 | |||
| b1fe34adfd | |||
| dcf3a03598 | |||
| 38a9f0d90f | |||
| 8b87c954eb | |||
| 1bc643408e | |||
| 875c8dd1c1 | |||
| 5834e067f4 | |||
| 79417e4971 | |||
| 304eb16e2e | |||
| df4cfc7e84 | |||
| 812beb2a02 |
72
CHANGELOG.md
72
CHANGELOG.md
@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.5] - 2026-01-23
|
||||
|
||||
### Added
|
||||
|
||||
- Admin dashboard widget showing license statistics on WordPress dashboard
|
||||
- Automatic license expiration via daily wp-cron job
|
||||
- License expired email notification sent when license auto-expires
|
||||
- New `LicenseExpiredEmail` WooCommerce email class (configurable via WooCommerce > Settings > Emails)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved download list layout in customer account licenses page
|
||||
- Downloads now displayed in two-row format: file link on first row, metadata on second row
|
||||
- Better visual separation between download link and version/date/checksum information
|
||||
|
||||
### Technical Details
|
||||
|
||||
- New `DashboardWidgetController` class in `src/Admin/` for WordPress dashboard widget
|
||||
- Widget displays: total licenses, active, expiring soon, expired counts, status breakdown, license types
|
||||
- New `LicenseExpiredEmail` class in `src/Email/` for expired license notifications
|
||||
- Added `getExpiredActiveLicenses()` and `autoExpireLicense()` methods to `LicenseManager`
|
||||
- Daily cron now auto-expires licenses with past expiration date and sends notification emails
|
||||
- Updated `templates/frontend/licenses.html.twig` with new two-row structure
|
||||
- Added `.download-item`, `.download-row-file`, `.download-row-meta` CSS classes
|
||||
- Improved responsive behavior for download metadata
|
||||
|
||||
## [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 +478,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
|
||||
|
||||
134
CLAUDE.md
134
CLAUDE.md
@@ -36,6 +36,10 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
||||
|
||||
No known bugs at the moment.
|
||||
|
||||
### Version 0.4.0
|
||||
|
||||
- On first plugin activation, get the checksums of all security related files (at least in `src/`) as hashes, store them encrypted on the server and add a mechanism to check the integrity of the files and the license validity periodically, control via wp-cron.
|
||||
|
||||
## Technical Stack
|
||||
|
||||
- **Language:** PHP 8.3.x
|
||||
@@ -840,3 +844,133 @@ 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()`
|
||||
|
||||
### 2026-01-23 - Version 0.3.5 - Dashboard Widget & Auto-Expire
|
||||
|
||||
**Overview:**
|
||||
|
||||
Added admin dashboard widget for license statistics and automatic license expiration via daily cron job.
|
||||
|
||||
**Implemented:**
|
||||
|
||||
- Admin dashboard widget showing license statistics (total, active, expiring soon, expired)
|
||||
- Status breakdown display with color-coded badges
|
||||
- License type breakdown (time-limited vs lifetime)
|
||||
- Daily wp-cron job to auto-expire licenses past their expiration date
|
||||
- License expired email notification sent when license auto-expires
|
||||
- Downloads in customer account now displayed in two-row format
|
||||
|
||||
**New files:**
|
||||
|
||||
- `src/Admin/DashboardWidgetController.php` - WordPress dashboard widget controller
|
||||
- `src/Email/LicenseExpiredEmail.php` - WooCommerce email for expired license notifications
|
||||
|
||||
**Modified files:**
|
||||
|
||||
- `src/Plugin.php` - Added DashboardWidgetController instantiation
|
||||
- `src/License/LicenseManager.php` - Added `getExpiredActiveLicenses()` and `autoExpireLicense()` methods
|
||||
- `src/Email/LicenseEmailController.php` - Added auto-expire logic and LicenseExpiredEmail registration
|
||||
- `templates/frontend/licenses.html.twig` - Restructured download list with two-row layout
|
||||
- `assets/css/frontend.css` - Added dashboard widget and download list styles
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- Dashboard widget uses `wp_add_dashboard_widget()` hook, requires `manage_woocommerce` capability
|
||||
- Widget displays statistics from existing `LicenseManager::getStatistics()` method
|
||||
- Auto-expire runs during daily `wclp_check_expiring_licenses` cron event
|
||||
- `getExpiredActiveLicenses()` finds licenses with past expiration date but still active status
|
||||
- `autoExpireLicense()` updates status to expired and returns true if changed
|
||||
- LicenseExpiredEmail follows same pattern as LicenseExpirationEmail (warning vs expired)
|
||||
- Expired notification tracked via user meta to prevent duplicate emails
|
||||
|
||||
36
README.md
36
README.md
@@ -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))
|
||||
|
||||
@@ -202,18 +202,30 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.download-list li {
|
||||
.download-list li.download-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding: 0.5em 0;
|
||||
flex-direction: column;
|
||||
gap: 0.35em;
|
||||
padding: 0.75em 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.download-list li:last-child {
|
||||
.download-list li.download-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.download-row-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.download-row-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -244,7 +256,6 @@
|
||||
.download-date {
|
||||
color: #999;
|
||||
font-size: 0.85em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.download-hash {
|
||||
@@ -338,15 +349,11 @@
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.download-list li {
|
||||
.download-row-meta {
|
||||
padding-left: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.download-date {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.woocommerce-licenses-table,
|
||||
.woocommerce-licenses-table thead,
|
||||
.woocommerce-licenses-table tbody,
|
||||
@@ -528,3 +535,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;
|
||||
}
|
||||
|
||||
@@ -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
15
composer.lock
generated
@@ -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",
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
48
openapi.json
48
openapi.json
@@ -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": [
|
||||
|
||||
1
releases/wc-licensed-product-0.3.3.sha256
Normal file
1
releases/wc-licensed-product-0.3.3.sha256
Normal file
@@ -0,0 +1 @@
|
||||
a06d29eabc2da08613ae13874ed152b8ea9363b8284a2e9bdda414e32777558c wc-licensed-product-0.3.3.zip
|
||||
BIN
releases/wc-licensed-product-0.3.3.zip
Normal file
BIN
releases/wc-licensed-product-0.3.3.zip
Normal file
Binary file not shown.
1
releases/wc-licensed-product-0.3.4.sha256
Normal file
1
releases/wc-licensed-product-0.3.4.sha256
Normal file
@@ -0,0 +1 @@
|
||||
36a81c00eb03adf5dfa633891664d44b7e5225bf1ee594904f8acc9adec6bb47 releases/wc-licensed-product-0.3.4.zip
|
||||
BIN
releases/wc-licensed-product-0.3.4.zip
Normal file
BIN
releases/wc-licensed-product-0.3.4.zip
Normal file
Binary file not shown.
@@ -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">×</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">×</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>
|
||||
|
||||
225
src/Admin/DashboardWidgetController.php
Normal file
225
src/Admin/DashboardWidgetController.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
/**
|
||||
* Dashboard Widget Controller
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Admin
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Admin;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\License;
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
|
||||
/**
|
||||
* Handles the WordPress admin dashboard widget for license statistics
|
||||
*/
|
||||
final class DashboardWidgetController
|
||||
{
|
||||
private LicenseManager $licenseManager;
|
||||
|
||||
public function __construct(LicenseManager $licenseManager)
|
||||
{
|
||||
$this->licenseManager = $licenseManager;
|
||||
$this->registerHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks
|
||||
*/
|
||||
private function registerHooks(): void
|
||||
{
|
||||
add_action('wp_dashboard_setup', [$this, 'registerDashboardWidget']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the dashboard widget
|
||||
*/
|
||||
public function registerDashboardWidget(): void
|
||||
{
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_add_dashboard_widget(
|
||||
'wclp_license_statistics',
|
||||
__('License Statistics', 'wc-licensed-product'),
|
||||
[$this, 'renderWidget']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the dashboard widget content
|
||||
*/
|
||||
public function renderWidget(): void
|
||||
{
|
||||
$stats = $this->licenseManager->getStatistics();
|
||||
$licensesUrl = admin_url('admin.php?page=wc-licensed-product-licenses');
|
||||
?>
|
||||
<style>
|
||||
.wclp-widget-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.wclp-stat-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e2e4e7;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.wclp-stat-card.highlight {
|
||||
border-left: 3px solid #7f54b3;
|
||||
}
|
||||
.wclp-stat-card.warning {
|
||||
border-left: 3px solid #f0b849;
|
||||
}
|
||||
.wclp-stat-card.danger {
|
||||
border-left: 3px solid #dc3232;
|
||||
}
|
||||
.wclp-stat-card.success {
|
||||
border-left: 3px solid #46b450;
|
||||
}
|
||||
.wclp-stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1d2327;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.wclp-stat-label {
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.wclp-widget-divider {
|
||||
border-top: 1px solid #e2e4e7;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.wclp-status-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.wclp-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.wclp-status-badge.active {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.wclp-status-badge.inactive {
|
||||
background: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
.wclp-status-badge.expired {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.wclp-status-badge.revoked {
|
||||
background: #d6d8db;
|
||||
color: #1b1e21;
|
||||
}
|
||||
.wclp-widget-footer {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e2e4e7;
|
||||
text-align: center;
|
||||
}
|
||||
.wclp-widget-footer a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wclp-widget-stats">
|
||||
<div class="wclp-stat-card highlight">
|
||||
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['total'])); ?></div>
|
||||
<div class="wclp-stat-label"><?php esc_html_e('Total Licenses', 'wc-licensed-product'); ?></div>
|
||||
</div>
|
||||
<div class="wclp-stat-card success">
|
||||
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['by_status'][License::STATUS_ACTIVE])); ?></div>
|
||||
<div class="wclp-stat-label"><?php esc_html_e('Active', 'wc-licensed-product'); ?></div>
|
||||
</div>
|
||||
<div class="wclp-stat-card warning">
|
||||
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['expiring_soon'])); ?></div>
|
||||
<div class="wclp-stat-label"><?php esc_html_e('Expiring Soon', 'wc-licensed-product'); ?></div>
|
||||
</div>
|
||||
<div class="wclp-stat-card danger">
|
||||
<div class="wclp-stat-number"><?php echo esc_html(number_format_i18n($stats['by_status'][License::STATUS_EXPIRED])); ?></div>
|
||||
<div class="wclp-stat-label"><?php esc_html_e('Expired', 'wc-licensed-product'); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wclp-widget-divider"></div>
|
||||
|
||||
<h4 style="margin: 0 0 8px 0; font-size: 13px; color: #1d2327;">
|
||||
<?php esc_html_e('Status Breakdown', 'wc-licensed-product'); ?>
|
||||
</h4>
|
||||
<div class="wclp-status-list">
|
||||
<span class="wclp-status-badge active">
|
||||
<span class="dashicons dashicons-yes-alt" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
||||
<?php printf(
|
||||
esc_html__('Active: %d', 'wc-licensed-product'),
|
||||
$stats['by_status'][License::STATUS_ACTIVE]
|
||||
); ?>
|
||||
</span>
|
||||
<span class="wclp-status-badge inactive">
|
||||
<span class="dashicons dashicons-marker" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
||||
<?php printf(
|
||||
esc_html__('Inactive: %d', 'wc-licensed-product'),
|
||||
$stats['by_status'][License::STATUS_INACTIVE]
|
||||
); ?>
|
||||
</span>
|
||||
<span class="wclp-status-badge expired">
|
||||
<span class="dashicons dashicons-clock" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
||||
<?php printf(
|
||||
esc_html__('Expired: %d', 'wc-licensed-product'),
|
||||
$stats['by_status'][License::STATUS_EXPIRED]
|
||||
); ?>
|
||||
</span>
|
||||
<span class="wclp-status-badge revoked">
|
||||
<span class="dashicons dashicons-dismiss" style="font-size: 14px; width: 14px; height: 14px;"></span>
|
||||
<?php printf(
|
||||
esc_html__('Revoked: %d', 'wc-licensed-product'),
|
||||
$stats['by_status'][License::STATUS_REVOKED]
|
||||
); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="wclp-widget-divider"></div>
|
||||
|
||||
<h4 style="margin: 0 0 8px 0; font-size: 13px; color: #1d2327;">
|
||||
<?php esc_html_e('License Types', 'wc-licensed-product'); ?>
|
||||
</h4>
|
||||
<p style="margin: 0; font-size: 13px; color: #646970;">
|
||||
<span class="dashicons dashicons-calendar-alt" style="font-size: 14px; width: 14px; height: 14px; vertical-align: text-bottom;"></span>
|
||||
<?php printf(
|
||||
esc_html__('Time-limited: %d', 'wc-licensed-product'),
|
||||
$stats['expiring']
|
||||
); ?>
|
||||
|
|
||||
<span class="dashicons dashicons-infinity" style="font-size: 14px; width: 14px; height: 14px; vertical-align: text-bottom;"></span>
|
||||
<?php printf(
|
||||
esc_html__('Lifetime: %d', 'wc-licensed-product'),
|
||||
$stats['lifetime']
|
||||
); ?>
|
||||
</p>
|
||||
|
||||
<div class="wclp-widget-footer">
|
||||
<a href="<?php echo esc_url($licensesUrl); ?>" class="button button-secondary">
|
||||
<?php esc_html_e('View All Licenses', 'wc-licensed-product'); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -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')]);
|
||||
|
||||
@@ -55,6 +55,7 @@ final class LicenseEmailController
|
||||
public function registerEmailClasses(array $email_classes): array
|
||||
{
|
||||
$email_classes['WCLP_License_Expiration'] = new LicenseExpirationEmail();
|
||||
$email_classes['WCLP_License_Expired'] = new LicenseExpiredEmail();
|
||||
return $email_classes;
|
||||
}
|
||||
|
||||
@@ -69,10 +70,13 @@ final class LicenseEmailController
|
||||
}
|
||||
|
||||
/**
|
||||
* Send expiration warning emails
|
||||
* Send expiration warning emails and auto-expire licenses
|
||||
*/
|
||||
public function sendExpirationWarnings(): void
|
||||
{
|
||||
// First, auto-expire licenses that have passed their expiration date
|
||||
$this->autoExpireAndNotify();
|
||||
|
||||
// Check if expiration emails are enabled in settings
|
||||
if (!SettingsController::isExpirationEmailsEnabled()) {
|
||||
return;
|
||||
@@ -107,6 +111,41 @@ final class LicenseEmailController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-expire licenses and send expired notifications
|
||||
*/
|
||||
private function autoExpireAndNotify(): void
|
||||
{
|
||||
// Get licenses that should be auto-expired
|
||||
$expiredActiveLicenses = $this->licenseManager->getExpiredActiveLicenses();
|
||||
|
||||
if (empty($expiredActiveLicenses)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the WooCommerce email instance for expired notifications
|
||||
$mailer = WC()->mailer();
|
||||
$emails = $mailer->get_emails();
|
||||
|
||||
/** @var LicenseExpiredEmail|null $expiredEmail */
|
||||
$expiredEmail = $emails['WCLP_License_Expired'] ?? null;
|
||||
|
||||
foreach ($expiredActiveLicenses as $license) {
|
||||
// Auto-expire the license
|
||||
$wasExpired = $this->licenseManager->autoExpireLicense($license->getId());
|
||||
|
||||
if ($wasExpired && $expiredEmail && $expiredEmail->is_enabled()) {
|
||||
// Check if we haven't already sent an expired notification
|
||||
if (!$this->licenseManager->wasExpirationNotified($license->getId(), 'license_expired')) {
|
||||
// Send expired notification email
|
||||
if ($expiredEmail->trigger($license)) {
|
||||
$this->licenseManager->markExpirationNotified($license->getId(), 'license_expired');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and send expiration warnings for a specific time frame
|
||||
*
|
||||
|
||||
335
src/Email/LicenseExpiredEmail.php
Normal file
335
src/Email/LicenseExpiredEmail.php
Normal file
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
/**
|
||||
* License Expired Email
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Email
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Email;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\License;
|
||||
use WC_Email;
|
||||
|
||||
/**
|
||||
* License Expired Email class
|
||||
*
|
||||
* Sends email notifications to customers when their licenses have expired.
|
||||
* Uses WooCommerce's transactional email system for consistent styling and customization.
|
||||
*/
|
||||
class LicenseExpiredEmail extends WC_Email
|
||||
{
|
||||
/**
|
||||
* License object
|
||||
*/
|
||||
public ?License $license = null;
|
||||
|
||||
/**
|
||||
* Product name
|
||||
*/
|
||||
public string $product_name = '';
|
||||
|
||||
/**
|
||||
* Expiration date formatted
|
||||
*/
|
||||
public string $expiration_date = '';
|
||||
|
||||
/**
|
||||
* Customer display name
|
||||
*/
|
||||
public string $customer_name = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = 'wclp_license_expired';
|
||||
$this->customer_email = true;
|
||||
$this->title = __('License Expired', 'wc-licensed-product');
|
||||
$this->description = __('License expired emails are sent to customers when their licenses have expired.', 'wc-licensed-product');
|
||||
|
||||
$this->placeholders = [
|
||||
'{site_title}' => $this->get_blogname(),
|
||||
'{product_name}' => '',
|
||||
'{expiration_date}' => '',
|
||||
];
|
||||
|
||||
// Call parent constructor
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email subject
|
||||
*/
|
||||
public function get_default_subject(): string
|
||||
{
|
||||
return __('[{site_title}] Your license for {product_name} has expired', 'wc-licensed-product');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email heading
|
||||
*/
|
||||
public function get_default_heading(): string
|
||||
{
|
||||
return __('License Expired', 'wc-licensed-product');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the email
|
||||
*
|
||||
* @param License $license License object
|
||||
*/
|
||||
public function trigger(License $license): bool
|
||||
{
|
||||
$this->setup_locale();
|
||||
|
||||
$customer = get_userdata($license->getCustomerId());
|
||||
if (!$customer || !$customer->user_email) {
|
||||
$this->restore_locale();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->license = $license;
|
||||
$this->recipient = $customer->user_email;
|
||||
$this->customer_name = $customer->display_name ?: __('Customer', 'wc-licensed-product');
|
||||
|
||||
$product = wc_get_product($license->getProductId());
|
||||
$this->product_name = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
||||
|
||||
$expiresAt = $license->getExpiresAt();
|
||||
$this->expiration_date = $expiresAt ? $expiresAt->format(get_option('date_format')) : '';
|
||||
|
||||
// Update placeholders
|
||||
$this->placeholders['{product_name}'] = $this->product_name;
|
||||
$this->placeholders['{expiration_date}'] = $this->expiration_date;
|
||||
|
||||
if (!$this->is_enabled() || !$this->get_recipient()) {
|
||||
$this->restore_locale();
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->send(
|
||||
$this->get_recipient(),
|
||||
$this->get_subject(),
|
||||
$this->get_content(),
|
||||
$this->get_headers(),
|
||||
$this->get_attachments()
|
||||
);
|
||||
|
||||
$this->restore_locale();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content HTML
|
||||
*/
|
||||
public function get_content_html(): string
|
||||
{
|
||||
ob_start();
|
||||
|
||||
// Use WooCommerce's email header
|
||||
wc_get_template('emails/email-header.php', ['email_heading' => $this->get_heading()]);
|
||||
|
||||
$this->render_email_body_html();
|
||||
|
||||
// Use WooCommerce's email footer
|
||||
wc_get_template('emails/email-footer.php', ['email' => $this]);
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content plain text
|
||||
*/
|
||||
public function get_content_plain(): string
|
||||
{
|
||||
ob_start();
|
||||
|
||||
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
|
||||
echo esc_html(wp_strip_all_tags($this->get_heading()));
|
||||
echo "\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n";
|
||||
|
||||
$this->render_email_body_plain();
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML email body content
|
||||
*/
|
||||
private function render_email_body_html(): void
|
||||
{
|
||||
$account_url = wc_get_account_endpoint_url('licenses');
|
||||
?>
|
||||
<p><?php printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($this->customer_name)); ?></p>
|
||||
|
||||
<p style="color: #dc3232; font-weight: 600;">
|
||||
<?php printf(
|
||||
esc_html__('Your license for %1$s has expired on %2$s.', 'wc-licensed-product'),
|
||||
'<strong>' . esc_html($this->product_name) . '</strong>',
|
||||
esc_html($this->expiration_date)
|
||||
); ?>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<?php esc_html_e('Your license is no longer valid and the product will stop working until you renew.', 'wc-licensed-product'); ?>
|
||||
</p>
|
||||
|
||||
<h2><?php esc_html_e('Expired License Details', 'wc-licensed-product'); ?></h2>
|
||||
|
||||
<div style="margin-bottom: 40px;">
|
||||
<table class="td" cellspacing="0" cellpadding="6" style="width: 100%; font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;" border="1">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Product:', 'wc-licensed-product'); ?></th>
|
||||
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php echo esc_html($this->product_name); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></th>
|
||||
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;">
|
||||
<code style="background: #f5f5f5; padding: 3px 8px; border-radius: 3px; font-family: monospace;">
|
||||
<?php echo esc_html($this->license->getLicenseKey()); ?>
|
||||
</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></th>
|
||||
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php echo esc_html($this->license->getDomain()); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Expired on:', 'wc-licensed-product'); ?></th>
|
||||
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>; color: #dc3232; font-weight: 600;"><?php echo esc_html($this->expiration_date); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="td" scope="row" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;"><?php esc_html_e('Status:', 'wc-licensed-product'); ?></th>
|
||||
<td class="td" style="text-align:<?php echo is_rtl() ? 'right' : 'left'; ?>;">
|
||||
<span style="background: #f8d7da; color: #721c24; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500;">
|
||||
<?php esc_html_e('Expired', 'wc-licensed-product'); ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$additional_content = $this->get_additional_content();
|
||||
if ($additional_content) :
|
||||
?>
|
||||
<p><?php echo wp_kses_post($additional_content); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="margin-top: 25px;">
|
||||
<a href="<?php echo esc_url($account_url); ?>" class="button" style="display: inline-block; background-color: #7f54b3; color: #ffffff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: 600;">
|
||||
<?php esc_html_e('View My Licenses', 'wc-licensed-product'); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render plain text email body content
|
||||
*/
|
||||
private function render_email_body_plain(): void
|
||||
{
|
||||
printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($this->customer_name));
|
||||
echo "\n\n";
|
||||
|
||||
printf(
|
||||
esc_html__('Your license for %1$s has expired on %2$s.', 'wc-licensed-product'),
|
||||
esc_html($this->product_name),
|
||||
esc_html($this->expiration_date)
|
||||
);
|
||||
echo "\n\n";
|
||||
|
||||
echo esc_html__('Your license is no longer valid and the product will stop working until you renew.', 'wc-licensed-product');
|
||||
echo "\n\n";
|
||||
|
||||
echo "----------\n";
|
||||
echo esc_html__('Expired License Details', 'wc-licensed-product') . "\n";
|
||||
echo "----------\n\n";
|
||||
|
||||
echo esc_html__('Product:', 'wc-licensed-product') . ' ' . esc_html($this->product_name) . "\n";
|
||||
echo esc_html__('License Key:', 'wc-licensed-product') . ' ' . esc_html($this->license->getLicenseKey()) . "\n";
|
||||
echo esc_html__('Domain:', 'wc-licensed-product') . ' ' . esc_html($this->license->getDomain()) . "\n";
|
||||
echo esc_html__('Expired on:', 'wc-licensed-product') . ' ' . esc_html($this->expiration_date) . "\n";
|
||||
echo esc_html__('Status:', 'wc-licensed-product') . ' ' . esc_html__('Expired', 'wc-licensed-product') . "\n\n";
|
||||
|
||||
$additional_content = $this->get_additional_content();
|
||||
if ($additional_content) {
|
||||
echo "----------\n\n";
|
||||
echo esc_html(wp_strip_all_tags(wptexturize($additional_content)));
|
||||
echo "\n\n";
|
||||
}
|
||||
|
||||
echo esc_html__('View My Licenses', 'wc-licensed-product') . ': ' . esc_url(wc_get_account_endpoint_url('licenses')) . "\n\n";
|
||||
|
||||
echo "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Default content to show below main email content
|
||||
*/
|
||||
public function get_default_additional_content(): string
|
||||
{
|
||||
return __('To continue using this product, please renew your license.', 'wc-licensed-product');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize settings form fields
|
||||
*/
|
||||
public function init_form_fields(): void
|
||||
{
|
||||
$placeholder_text = sprintf(
|
||||
/* translators: %s: list of placeholders */
|
||||
__('Available placeholders: %s', 'wc-licensed-product'),
|
||||
'<code>{site_title}, {product_name}, {expiration_date}</code>'
|
||||
);
|
||||
|
||||
$this->form_fields = [
|
||||
'enabled' => [
|
||||
'title' => __('Enable/Disable', 'wc-licensed-product'),
|
||||
'type' => 'checkbox',
|
||||
'label' => __('Enable this email notification', 'wc-licensed-product'),
|
||||
'default' => 'yes',
|
||||
],
|
||||
'subject' => [
|
||||
'title' => __('Subject', 'wc-licensed-product'),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'description' => $placeholder_text,
|
||||
'placeholder' => $this->get_default_subject(),
|
||||
'default' => '',
|
||||
],
|
||||
'heading' => [
|
||||
'title' => __('Email heading', 'wc-licensed-product'),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'description' => $placeholder_text,
|
||||
'placeholder' => $this->get_default_heading(),
|
||||
'default' => '',
|
||||
],
|
||||
'additional_content' => [
|
||||
'title' => __('Additional content', 'wc-licensed-product'),
|
||||
'description' => __('Text to appear below the main email content.', 'wc-licensed-product') . ' ' . $placeholder_text,
|
||||
'css' => 'width:400px; height: 75px;',
|
||||
'placeholder' => $this->get_default_additional_content(),
|
||||
'type' => 'textarea',
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
],
|
||||
'email_type' => [
|
||||
'title' => __('Email type', 'wc-licensed-product'),
|
||||
'type' => 'select',
|
||||
'description' => __('Choose which format of email to send.', 'wc-licensed-product'),
|
||||
'default' => 'html',
|
||||
'class' => 'email_type wc-enhanced-select',
|
||||
'options' => $this->get_email_type_options(),
|
||||
'desc_tip' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -862,6 +862,56 @@ class LicenseManager
|
||||
return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get licenses that have passed their expiration date but are still marked as active
|
||||
*
|
||||
* @return array Array of License objects that need to be auto-expired
|
||||
*/
|
||||
public function getExpiredActiveLicenses(): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
$sql = "SELECT * FROM {$tableName}
|
||||
WHERE expires_at IS NOT NULL
|
||||
AND expires_at < %s
|
||||
AND status = %s";
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare($sql, $now->format('Y-m-d H:i:s'), License::STATUS_ACTIVE),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-expire a license and return true if status was changed
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @return bool True if license was expired, false if already expired or error
|
||||
*/
|
||||
public function autoExpireLicense(int $licenseId): bool
|
||||
{
|
||||
$license = $this->getLicenseById($licenseId);
|
||||
if (!$license) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only expire if currently active and past expiration date
|
||||
if ($license->getStatus() !== License::STATUS_ACTIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$license->isExpired()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->updateLicenseStatus($licenseId, License::STATUS_EXPIRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a license from CSV data
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Jeremias\WcLicensedProduct;
|
||||
|
||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||
use Jeremias\WcLicensedProduct\Admin\DashboardWidgetController;
|
||||
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
||||
@@ -151,6 +152,7 @@ final class Plugin
|
||||
new VersionAdminController($this->versionManager);
|
||||
new OrderLicenseController($this->licenseManager);
|
||||
new SettingsController();
|
||||
new DashboardWidgetController($this->licenseManager);
|
||||
|
||||
// Show admin notice if unlicensed and not on localhost
|
||||
if (!$isLicensed && !$licenseChecker->isLocalhost()) {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">×</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>
|
||||
|
||||
@@ -57,12 +57,14 @@
|
||||
<h4>{{ __('Available Downloads') }}</h4>
|
||||
<ul class="download-list">
|
||||
{% for download in item.downloads %}
|
||||
<li>
|
||||
<li class="download-item">
|
||||
<div class="download-row-file">
|
||||
<a href="{{ esc_url(download.download_url) }}" class="download-link">
|
||||
<span class="dashicons dashicons-download"></span>
|
||||
{{ esc_html(download.filename ?: 'Version ' ~ download.version) }}
|
||||
</a>
|
||||
<span class="download-version">v{{ esc_html(download.version) }}</span>
|
||||
</div>
|
||||
<div class="download-row-meta">
|
||||
<span class="download-date">{{ esc_html(download.released_at) }}</span>
|
||||
{% if download.file_hash %}
|
||||
<span class="download-hash" title="{{ esc_attr(download.file_hash) }}">
|
||||
@@ -70,6 +72,7 @@
|
||||
<code>{{ download.file_hash[:12] }}...</code>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -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.5
|
||||
* 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.5');
|
||||
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__));
|
||||
|
||||
Reference in New Issue
Block a user