You've already forked wc-licensed-product
Implement versions 0.0.4-0.0.7 features
v0.0.4: - Add WooCommerce settings tab for default license settings - Per-product settings override global defaults v0.0.5: - Add bulk license operations (activate, deactivate, revoke, extend, delete) - Add license renewal/extension and lifetime functionality - Add quick action buttons per license row v0.0.6: - Add license dashboard with statistics and analytics - Add license transfer functionality (admin) - Add CSV export for licenses - Add OpenAPI 3.1 specification - Remove /deactivate API endpoint v0.0.7: - Move license dashboard to WooCommerce Reports section - Add license search and filtering in admin - Add customer-facing license transfer with AJAX modal - Add email notifications for license expiration warnings - Add bulk import licenses from CSV - Update README with comprehensive documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
111
CHANGELOG.md
111
CHANGELOG.md
@@ -7,6 +7,110 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.0.7] - 2026-01-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- License Dashboard moved to WooCommerce Reports section (Reports > Licenses)
|
||||||
|
- License search and filtering in admin (by license key, domain, status, product)
|
||||||
|
- Customer-facing license transfer request with AJAX-based modal
|
||||||
|
- Email notifications for license expiration warnings (7 days and 1 day before)
|
||||||
|
- Bulk import licenses from CSV functionality
|
||||||
|
- Import page with detailed format instructions
|
||||||
|
- Scheduled cron job for daily expiration checks
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Dashboard now accessible via WooCommerce > Reports > Licenses tab
|
||||||
|
- License list page includes search box and filter dropdowns
|
||||||
|
- Pagination preserves filter state
|
||||||
|
- Import CSV button added to licenses page header
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- AccountController extended with customer transfer AJAX handler and domain normalization
|
||||||
|
- LicenseEmailController extended with expiration warning scheduling and email templates
|
||||||
|
- LicenseManager extended with `getLicensesExpiringSoon()`, `markExpirationNotified()`, `wasExpirationNotified()`, `importLicense()` methods
|
||||||
|
- AdminController extended with CSV import handling and import page rendering
|
||||||
|
- Installer clears cron events on plugin deactivation
|
||||||
|
- Frontend JavaScript extended for transfer modal handling
|
||||||
|
- Frontend CSS extended with modal and transfer button styles
|
||||||
|
|
||||||
|
## [0.0.6] - 2026-01-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- License usage statistics/analytics dashboard (WooCommerce > License Dashboard)
|
||||||
|
- License transfer functionality to change domain from admin
|
||||||
|
- Export licenses to CSV functionality
|
||||||
|
- OpenAPI 3.1 specification for REST API documentation (`openapi.json`)
|
||||||
|
- Monthly license creation chart on dashboard
|
||||||
|
- Top products and top domains statistics
|
||||||
|
- Expiring soon alerts on dashboard
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- API endpoint `/deactivate` removed (license deactivation is now admin-only)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Licenses admin page header now includes Export CSV button
|
||||||
|
- Actions column widened to accommodate Transfer link
|
||||||
|
- Dashboard link added to licenses page
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- New admin page: License Dashboard with statistics overview
|
||||||
|
- LicenseManager extended with `transferLicense()`, `getStatistics()`, `exportLicensesForCsv()` methods
|
||||||
|
- AdminController extended with dashboard rendering, CSV export, and transfer handling
|
||||||
|
- Transfer modal with form for domain change
|
||||||
|
- REST API now only has three endpoints: `/validate`, `/status`, `/activate`
|
||||||
|
- OpenAPI 3.1 specification documents all API endpoints with examples
|
||||||
|
|
||||||
|
## [0.0.5] - 2026-01-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Bulk license operations in admin (activate, deactivate, revoke, extend, delete)
|
||||||
|
- License renewal/extension functionality (extend by 30/90/365 days)
|
||||||
|
- Set license to lifetime (remove expiration)
|
||||||
|
- Quick action buttons per license row (+30d, ∞, Revoke, Delete)
|
||||||
|
- Checkbox selection for bulk operations with select-all functionality
|
||||||
|
- Improved admin notices for bulk operation results
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Licenses admin page redesigned with WordPress list table styling
|
||||||
|
- License actions use row-actions pattern for cleaner UI
|
||||||
|
- Bulk action dropdowns at top and bottom of table (synchronized)
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- LicenseManager extended with `extendLicense()`, `setLicenseLifetime()`, `bulkUpdateStatus()`, `bulkDelete()`, `bulkExtend()` methods
|
||||||
|
- AdminController extended with bulk action handling and extend/lifetime actions
|
||||||
|
- Twig template functions for generating action URLs with nonces
|
||||||
|
- JavaScript for checkbox sync and bulk action handling
|
||||||
|
|
||||||
|
## [0.0.4] - 2026-01-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- WooCommerce settings tab "Licensed Products" for default license settings
|
||||||
|
- Default settings for Max Activations, License Validity, and Bind to Major Version
|
||||||
|
- Per-product settings now override global defaults (empty = use default)
|
||||||
|
- Settings link in product edit page pointing to WooCommerce settings
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Product license settings now show placeholder with default values
|
||||||
|
- LicensedProduct class now falls back to global defaults when product settings are empty
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- New class: SettingsController for WooCommerce settings integration
|
||||||
|
- LicensedProduct model extended with `has_custom_*` methods for checking overrides
|
||||||
|
- Settings stored using WooCommerce options API
|
||||||
|
|
||||||
## [0.0.3] - 2026-01-21
|
## [0.0.3] - 2026-01-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -67,7 +171,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- `POST /wp-json/wc-licensed-product/v1/validate` - Validate license for domain
|
- `POST /wp-json/wc-licensed-product/v1/validate` - Validate license for domain
|
||||||
- `POST /wp-json/wc-licensed-product/v1/status` - Check license status
|
- `POST /wp-json/wc-licensed-product/v1/status` - Check license status
|
||||||
- `POST /wp-json/wc-licensed-product/v1/activate` - Activate license on domain
|
- `POST /wp-json/wc-licensed-product/v1/activate` - Activate license on domain
|
||||||
- `POST /wp-json/wc-licensed-product/v1/deactivate` - Deactivate license
|
|
||||||
- Checkout domain field for licensed products
|
- Checkout domain field for licensed products
|
||||||
- Customer account page "Licenses" to view purchased licenses
|
- Customer account page "Licenses" to view purchased licenses
|
||||||
- Admin interface for license management (WooCommerce > Licenses)
|
- Admin interface for license management (WooCommerce > Licenses)
|
||||||
@@ -92,7 +195,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- WordPress REST API integration
|
- WordPress REST API integration
|
||||||
- Custom WooCommerce product type extending WC_Product
|
- Custom WooCommerce product type extending WC_Product
|
||||||
|
|
||||||
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.3...HEAD
|
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.7...HEAD
|
||||||
|
[0.0.7]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.6...v0.0.7
|
||||||
|
[0.0.6]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.5...v0.0.6
|
||||||
|
[0.0.5]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.4...v0.0.5
|
||||||
|
[0.0.4]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.3...v0.0.4
|
||||||
[0.0.3]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.2...v0.0.3
|
[0.0.3]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.2...v0.0.3
|
||||||
[0.0.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.1...v0.0.2
|
[0.0.2]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/compare/v0.0.1...v0.0.2
|
||||||
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/releases/tag/v0.0.1
|
[0.0.1]: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/releases/tag/v0.0.1
|
||||||
|
|||||||
125
CLAUDE.md
125
CLAUDE.md
@@ -36,11 +36,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
_No known bugs at this time._
|
_No known bugs at this time._
|
||||||
|
|
||||||
### Version 0.0.4 (Next)
|
### Version 0.0.8 (Next)
|
||||||
|
|
||||||
- Consider adding license usage statistics/analytics
|
_No planned features yet. Add items as needed._
|
||||||
- Consider adding bulk license operations in admin
|
|
||||||
- Consider adding license renewal/extension functionality
|
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -226,11 +224,12 @@ Created on plugin activation via `Installer::createTables()`:
|
|||||||
Base: `/wp-json/wc-licensed-product/v1/`
|
Base: `/wp-json/wc-licensed-product/v1/`
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
| Endpoint | Method | Description |
|
||||||
| -------------- | ------ | -------------------------------- |
|
| ----------- | ------ | ------------------------------- |
|
||||||
| `/validate` | POST | Validate license key for domain |
|
| `/validate` | POST | Validate license key for domain |
|
||||||
| `/status` | POST | Get license status |
|
| `/status` | POST | Get license status |
|
||||||
| `/activate` | POST | Activate license on domain |
|
| `/activate` | POST | Activate license on domain |
|
||||||
| `/deactivate` | POST | Deactivate license |
|
|
||||||
|
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
||||||
|
|
||||||
### Key Classes
|
### Key Classes
|
||||||
|
|
||||||
@@ -316,3 +315,117 @@ Base: `/wp-json/wc-licensed-product/v1/`
|
|||||||
**Bug fixes:**
|
**Bug fixes:**
|
||||||
|
|
||||||
- Fixed product versions meta box visibility for non-licensed product types (targets `#wc_licensed_product_versions` container)
|
- Fixed product versions meta box visibility for non-licensed product types (targets `#wc_licensed_product_versions` container)
|
||||||
|
|
||||||
|
### 2026-01-21 - Version 0.0.4 Features
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- WooCommerce settings tab "Licensed Products" for default license settings
|
||||||
|
- Default settings for Max Activations, License Validity, and Bind to Major Version
|
||||||
|
- Per-product settings override global defaults (empty = use default)
|
||||||
|
- Settings link in product edit page pointing to WooCommerce settings
|
||||||
|
|
||||||
|
**New classes:**
|
||||||
|
|
||||||
|
- `SettingsController` - WooCommerce settings tab integration
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Settings stored using WooCommerce options API
|
||||||
|
- LicensedProduct model falls back to global defaults when product settings are empty
|
||||||
|
- Added `has_custom_*` methods to check if product has overridden defaults
|
||||||
|
- Product edit panel shows placeholders with default values
|
||||||
|
|
||||||
|
### 2026-01-21 - Version 0.0.5 Features
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- Bulk license operations in admin (activate, deactivate, revoke, extend, delete)
|
||||||
|
- License renewal/extension functionality (extend by 30/90/365 days)
|
||||||
|
- Set license to lifetime (remove expiration)
|
||||||
|
- Quick action buttons per license row (+30d, ∞, Revoke, Delete)
|
||||||
|
- Checkbox selection with select-all for bulk operations
|
||||||
|
- Improved admin notices for bulk operation results
|
||||||
|
|
||||||
|
**New methods in LicenseManager:**
|
||||||
|
|
||||||
|
- `extendLicense()` - Extend license by specified days
|
||||||
|
- `setLicenseLifetime()` - Remove expiration (set to lifetime)
|
||||||
|
- `bulkUpdateStatus()` - Bulk update license status
|
||||||
|
- `bulkDelete()` - Bulk delete licenses
|
||||||
|
- `bulkExtend()` - Bulk extend licenses
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Admin page redesigned with WordPress list table styling
|
||||||
|
- Twig template functions for generating action URLs with nonces
|
||||||
|
- JavaScript for checkbox synchronization and bulk action handling
|
||||||
|
- Row-actions pattern for cleaner per-license action UI
|
||||||
|
|
||||||
|
### 2026-01-21 - Version 0.0.6 Features
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- License usage statistics/analytics dashboard (WooCommerce > License Dashboard)
|
||||||
|
- License transfer functionality (change domain from admin)
|
||||||
|
- Export licenses to CSV with all related data
|
||||||
|
- OpenAPI 3.1 specification for REST API (`openapi.json`)
|
||||||
|
- Removed `/deactivate` API endpoint (admin-only operation now)
|
||||||
|
|
||||||
|
**New methods in LicenseManager:**
|
||||||
|
|
||||||
|
- `transferLicense()` - Transfer license to new domain
|
||||||
|
- `getStatistics()` - Get comprehensive license statistics
|
||||||
|
- `exportLicensesForCsv()` - Export all licenses for CSV download
|
||||||
|
|
||||||
|
**New files:**
|
||||||
|
|
||||||
|
- `templates/admin/dashboard.html.twig` - Dashboard template
|
||||||
|
- `openapi.json` - OpenAPI 3.1 specification
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- Dashboard shows status cards, monthly chart, top products/domains
|
||||||
|
- Transfer uses modal dialog with form submission
|
||||||
|
- CSV export includes BOM for UTF-8 Excel compatibility
|
||||||
|
- OpenAPI spec documents all endpoints with examples and error responses
|
||||||
|
- Statistics query uses SQL aggregation for performance
|
||||||
|
|
||||||
|
### 2026-01-21 - Version 0.0.7 Features
|
||||||
|
|
||||||
|
**Implemented:**
|
||||||
|
|
||||||
|
- License Dashboard moved to WooCommerce Reports section (Reports > Licenses tab)
|
||||||
|
- License search and filtering in admin (by key, domain, status, product)
|
||||||
|
- Customer-facing license transfer request with AJAX modal
|
||||||
|
- Email notifications for license expiration warnings (7 days and 1 day before)
|
||||||
|
- Bulk import licenses from CSV with validation and options
|
||||||
|
|
||||||
|
**New methods in LicenseManager:**
|
||||||
|
|
||||||
|
- `getLicensesExpiringSoon()` - Get licenses expiring within specified days
|
||||||
|
- `markExpirationNotified()` - Track expiration notification state
|
||||||
|
- `wasExpirationNotified()` - Check if notification already sent
|
||||||
|
- `importLicense()` - Create license from imported data
|
||||||
|
|
||||||
|
**Updated controllers:**
|
||||||
|
|
||||||
|
- `AdminController` - Added Reports integration, search/filter handling, CSV import
|
||||||
|
- `AccountController` - Added customer transfer AJAX handler with domain validation
|
||||||
|
- `LicenseEmailController` - Added cron scheduling and expiration warning emails
|
||||||
|
- `Installer` - Clears cron events on deactivation
|
||||||
|
|
||||||
|
**New UI features:**
|
||||||
|
|
||||||
|
- Search box and filter dropdowns on licenses page
|
||||||
|
- Transfer button and modal on customer licenses page
|
||||||
|
- Import CSV page with format documentation
|
||||||
|
- Pagination preserves filter state
|
||||||
|
|
||||||
|
**Technical notes:**
|
||||||
|
|
||||||
|
- WooCommerce Reports integration uses `woocommerce_admin_reports` filter
|
||||||
|
- Customer transfer uses AJAX with nonce verification
|
||||||
|
- Expiration warnings use WordPress cron with daily schedule
|
||||||
|
- CSV import supports both exported format and simplified format
|
||||||
|
- User meta tracks expiration notifications to prevent duplicates
|
||||||
|
|||||||
133
README.md
133
README.md
@@ -8,14 +8,34 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
### Core Features
|
||||||
|
|
||||||
- **Licensed Product Type**: New WooCommerce product type for software sales
|
- **Licensed Product Type**: New WooCommerce product type for software sales
|
||||||
- **Automatic License Generation**: License keys generated on order completion
|
- **Automatic License Generation**: License keys generated on order completion (format: XXXX-XXXX-XXXX-XXXX)
|
||||||
- **Domain Binding**: Licenses are bound to customer-specified domains
|
- **Domain Binding**: Licenses are bound to customer-specified domains
|
||||||
- **REST API**: Public endpoints for license validation and management
|
- **REST API**: Public endpoints for license validation and management
|
||||||
- **Customer Account**: Customers can view their licenses in My Account
|
|
||||||
- **Admin Management**: Full CRUD interface for license management
|
|
||||||
- **Version Binding**: Optional binding to major software versions
|
- **Version Binding**: Optional binding to major software versions
|
||||||
- **Expiration Support**: Set license validity periods or lifetime licenses
|
- **Expiration Support**: Set license validity periods or lifetime licenses
|
||||||
|
- **Rate Limiting**: API endpoints protected with rate limiting (30 requests/minute)
|
||||||
|
|
||||||
|
### Customer Features
|
||||||
|
|
||||||
|
- **My Account Licenses**: Customers can view their licenses in My Account
|
||||||
|
- **License Transfers**: Customers can transfer licenses to new domains
|
||||||
|
- **Secure Downloads**: Download purchased software versions with license verification
|
||||||
|
- **Copy to Clipboard**: Easy license key copying
|
||||||
|
|
||||||
|
### Admin Features
|
||||||
|
|
||||||
|
- **License Management**: Full CRUD interface for license management
|
||||||
|
- **License Dashboard**: Statistics and analytics (WooCommerce > Reports > Licenses)
|
||||||
|
- **Search & Filtering**: Search by license key, domain, status, or product
|
||||||
|
- **Bulk Operations**: Activate, deactivate, revoke, extend, or delete multiple licenses
|
||||||
|
- **License Transfer**: Transfer licenses to new domains
|
||||||
|
- **CSV Export/Import**: Export and import licenses via CSV
|
||||||
|
- **Expiration Warnings**: Automatic email notifications before license expiration
|
||||||
|
- **Version Management**: Manage multiple versions per product with file attachments
|
||||||
|
- **Global Settings**: Default license settings via WooCommerce settings tab
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -42,6 +62,19 @@ WC Licensed Product adds a new product type "Licensed Product" to WooCommerce, e
|
|||||||
- **Bind to Major Version**: Lock license to current major version
|
- **Bind to Major Version**: Lock license to current major version
|
||||||
- **Current Version**: Your software's current version
|
- **Current Version**: Your software's current version
|
||||||
|
|
||||||
|
### Managing Product Versions
|
||||||
|
|
||||||
|
1. Edit a Licensed Product
|
||||||
|
2. Use the "Product Versions" meta box to add versions
|
||||||
|
3. Upload version files via WordPress Media Library
|
||||||
|
4. Version numbers are auto-detected from filenames (e.g., `plugin-v1.2.3.zip`)
|
||||||
|
|
||||||
|
### Global Default Settings
|
||||||
|
|
||||||
|
1. Go to WooCommerce > Settings > Licensed Products
|
||||||
|
2. Set default values for Max Activations, License Validity, and Version Binding
|
||||||
|
3. Per-product settings override these defaults
|
||||||
|
|
||||||
### Customer Checkout
|
### Customer Checkout
|
||||||
|
|
||||||
When a customer purchases a licensed product, they must enter the domain where they will use the license during checkout.
|
When a customer purchases a licensed product, they must enter the domain where they will use the license during checkout.
|
||||||
@@ -50,11 +83,30 @@ When a customer purchases a licensed product, they must enter the domain where t
|
|||||||
|
|
||||||
- **Customers**: My Account > Licenses
|
- **Customers**: My Account > Licenses
|
||||||
- **Administrators**: WooCommerce > Licenses
|
- **Administrators**: WooCommerce > Licenses
|
||||||
|
- **Dashboard**: WooCommerce > Reports > Licenses (for statistics)
|
||||||
|
|
||||||
|
### Exporting & Importing Licenses
|
||||||
|
|
||||||
|
**Export:**
|
||||||
|
|
||||||
|
1. Go to WooCommerce > Licenses
|
||||||
|
2. Click "Export CSV" to download all licenses
|
||||||
|
|
||||||
|
**Import:**
|
||||||
|
|
||||||
|
1. Go to WooCommerce > Licenses
|
||||||
|
2. Click "Import CSV"
|
||||||
|
3. Upload a CSV file (supports exported format or simplified format)
|
||||||
|
4. Choose options: skip header row, update existing licenses
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
|
|
||||||
|
Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
||||||
|
|
||||||
### Validate License
|
### Validate License
|
||||||
|
|
||||||
|
Validate a license key for a specific domain.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /wp-json/wc-licensed-product/v1/validate
|
POST /wp-json/wc-licensed-product/v1/validate
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -65,8 +117,33 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Success Response (200):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"license": {
|
||||||
|
"product_id": 123,
|
||||||
|
"expires_at": "2027-01-21",
|
||||||
|
"version_id": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (403):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": false,
|
||||||
|
"error": "domain_mismatch",
|
||||||
|
"message": "This license is not valid for this domain."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Check Status
|
### Check Status
|
||||||
|
|
||||||
|
Get detailed license status information.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /wp-json/wc-licensed-product/v1/status
|
POST /wp-json/wc-licensed-product/v1/status
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -76,8 +153,23 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"status": "active",
|
||||||
|
"domain": "example.com",
|
||||||
|
"expires_at": "2027-01-21",
|
||||||
|
"activations_count": 1,
|
||||||
|
"max_activations": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Activate License
|
### Activate License
|
||||||
|
|
||||||
|
Activate a license on a domain.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /wp-json/wc-licensed-product/v1/activate
|
POST /wp-json/wc-licensed-product/v1/activate
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@@ -88,18 +180,27 @@ Content-Type: application/json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deactivate License
|
**Response (200):**
|
||||||
|
|
||||||
```http
|
|
||||||
POST /wp-json/wc-licensed-product/v1/deactivate
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
"success": true,
|
||||||
"domain": "example.com"
|
"message": "License activated successfully."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Error Codes
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| `license_not_found` | License key does not exist |
|
||||||
|
| `license_revoked` | License has been revoked |
|
||||||
|
| `license_expired` | License has expired |
|
||||||
|
| `license_inactive` | License is inactive |
|
||||||
|
| `domain_mismatch` | License not valid for this domain |
|
||||||
|
| `max_activations_reached` | Maximum activations reached |
|
||||||
|
| `rate_limit_exceeded` | Too many requests (wait and retry) |
|
||||||
|
|
||||||
## License Statuses
|
## License Statuses
|
||||||
|
|
||||||
- **Active**: License is valid and usable
|
- **Active**: License is valid and usable
|
||||||
@@ -107,6 +208,18 @@ Content-Type: application/json
|
|||||||
- **Expired**: License validity period has ended
|
- **Expired**: License validity period has ended
|
||||||
- **Revoked**: License has been manually revoked by admin
|
- **Revoked**: License has been manually revoked by admin
|
||||||
|
|
||||||
|
## Email Notifications
|
||||||
|
|
||||||
|
The plugin sends automatic email notifications:
|
||||||
|
|
||||||
|
- **Order Completion**: License keys included in order confirmation emails
|
||||||
|
- **Expiration Warning (7 days)**: Reminder sent 7 days before expiration
|
||||||
|
- **Expiration Warning (1 day)**: Urgent reminder sent 1 day before expiration
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues and feature requests, please visit:
|
For issues and feature requests, please visit:
|
||||||
|
|||||||
@@ -165,3 +165,306 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Licenses Table Bulk Actions */
|
||||||
|
.licenses-table {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licenses-table .check-column {
|
||||||
|
width: 2.2em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licenses-table .license-actions {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licenses-table .row-actions {
|
||||||
|
visibility: visible;
|
||||||
|
padding: 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licenses-table .row-actions a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licenses-table .row-actions .submitdelete {
|
||||||
|
color: #b32d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licenses-table .row-actions .submitdelete:hover {
|
||||||
|
color: #dc3232;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lifetime Badge */
|
||||||
|
.license-lifetime {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
color: #2271b1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulk Actions Styling */
|
||||||
|
.tablenav .actions select {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tablenav .bulkactions {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transfer Modal */
|
||||||
|
.wclp-modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-content {
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 10% auto;
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 90%;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-close:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal .form-table th {
|
||||||
|
padding: 15px 10px 15px 0;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal .submit {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding-top: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Styles */
|
||||||
|
.wclp-dashboard-stats {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-card {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 200px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-icon .dashicons {
|
||||||
|
font-size: 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-number {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #646970;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-total .wclp-stat-icon .dashicons {
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-active .wclp-stat-icon .dashicons {
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-inactive .wclp-stat-icon .dashicons {
|
||||||
|
color: #dba617;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-expired .wclp-stat-icon .dashicons {
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-revoked .wclp-stat-icon .dashicons {
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-box {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 280px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-box.wclp-stat-full {
|
||||||
|
flex: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-box h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-box .widefat {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-value {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-stat-value.wclp-warning {
|
||||||
|
color: #d63638;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Bar Chart */
|
||||||
|
.wclp-chart-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-bar-chart {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
height: 200px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-bar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-bar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 40px;
|
||||||
|
background: linear-gradient(180deg, #2271b1, #135e96);
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
min-height: 4px;
|
||||||
|
position: relative;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-bar:hover {
|
||||||
|
background: linear-gradient(180deg, #135e96, #0a4b78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-bar-value {
|
||||||
|
position: absolute;
|
||||||
|
top: -20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-bar-label {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #646970;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Actions */
|
||||||
|
.wclp-dashboard-actions {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-dashboard-actions h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-action-buttons .button .dashicons {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wider actions column for transfer link */
|
||||||
|
.licenses-table .license-actions {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,6 +150,39 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.license-domain-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transfer Button */
|
||||||
|
.wclp-transfer-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
padding: 0.2em 0.6em;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-transfer-btn:hover {
|
||||||
|
background: #e5e5e5;
|
||||||
|
border-color: #ccc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-transfer-btn .dashicons {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Download Section */
|
/* Download Section */
|
||||||
.license-downloads {
|
.license-downloads {
|
||||||
padding: 1em 1.5em;
|
padding: 1em 1.5em;
|
||||||
@@ -323,4 +356,151 @@
|
|||||||
width: 45%;
|
width: 45%;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.license-domain-display {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-transfer-btn {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transfer Modal */
|
||||||
|
.wclp-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 100000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-content {
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5em;
|
||||||
|
right: 0.5em;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-close:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-modal-content h3 {
|
||||||
|
margin: 0 0 1.5em 0;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-form-row {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-form-row label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-form-row input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75em;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-form-row input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2271b1;
|
||||||
|
box-shadow: 0 0 0 1px #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-current-domain {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-current-domain code {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-field-description {
|
||||||
|
margin: 0.5em 0 0 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-btn-primary {
|
||||||
|
background: #2271b1 !important;
|
||||||
|
border-color: #2271b1 !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-btn-primary:hover {
|
||||||
|
background: #135e96 !important;
|
||||||
|
border-color: #135e96 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-message {
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 0.75em 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wclp-message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,29 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var WCLicensedProductFrontend = {
|
var WCLicensedProductFrontend = {
|
||||||
|
$modal: null,
|
||||||
|
$form: null,
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
|
this.$modal = $('#wclp-transfer-modal');
|
||||||
|
this.$form = $('#wclp-transfer-form');
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
},
|
},
|
||||||
|
|
||||||
bindEvents: function() {
|
bindEvents: function() {
|
||||||
$(document).on('click', '.copy-license-btn', this.copyLicenseKey);
|
$(document).on('click', '.copy-license-btn', this.copyLicenseKey);
|
||||||
|
|
||||||
|
// Transfer modal events
|
||||||
|
$(document).on('click', '.wclp-transfer-btn', this.openTransferModal.bind(this));
|
||||||
|
$(document).on('click', '.wclp-modal-close, .wclp-modal-cancel, .wclp-modal-overlay', this.closeTransferModal.bind(this));
|
||||||
|
$(document).on('submit', '#wclp-transfer-form', this.submitTransfer.bind(this));
|
||||||
|
|
||||||
|
// Close modal on escape key
|
||||||
|
$(document).on('keyup', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
WCLicensedProductFrontend.closeTransferModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,6 +97,106 @@
|
|||||||
$(this).remove();
|
$(this).remove();
|
||||||
});
|
});
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open transfer modal
|
||||||
|
*/
|
||||||
|
openTransferModal: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(e.currentTarget);
|
||||||
|
var licenseId = $btn.data('license-id');
|
||||||
|
var currentDomain = $btn.data('current-domain');
|
||||||
|
|
||||||
|
$('#transfer-license-id').val(licenseId);
|
||||||
|
$('#transfer-current-domain').text(currentDomain);
|
||||||
|
$('#transfer-new-domain').val('');
|
||||||
|
$('#wclp-transfer-message').hide().removeClass('success error');
|
||||||
|
$('#wclp-transfer-submit').prop('disabled', false);
|
||||||
|
|
||||||
|
this.$modal.show();
|
||||||
|
$('#transfer-new-domain').focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close transfer modal
|
||||||
|
*/
|
||||||
|
closeTransferModal: function(e) {
|
||||||
|
if (e && $(e.target).closest('.wclp-modal-content').length && !$(e.target).is('.wclp-modal-close, .wclp-modal-cancel')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$modal.hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit transfer request
|
||||||
|
*/
|
||||||
|
submitTransfer: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var licenseId = $('#transfer-license-id').val();
|
||||||
|
var newDomain = $('#transfer-new-domain').val().trim();
|
||||||
|
var $message = $('#wclp-transfer-message');
|
||||||
|
var $submit = $('#wclp-transfer-submit');
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!newDomain) {
|
||||||
|
$message.text(wcLicensedProduct.strings.invalidDomain)
|
||||||
|
.removeClass('success').addClass('error').show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm transfer
|
||||||
|
if (!confirm(wcLicensedProduct.strings.transferConfirm)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable submit button
|
||||||
|
$submit.prop('disabled', true);
|
||||||
|
$message.hide();
|
||||||
|
|
||||||
|
// Send AJAX request
|
||||||
|
$.ajax({
|
||||||
|
url: wcLicensedProduct.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wclp_customer_transfer_license',
|
||||||
|
nonce: wcLicensedProduct.transferNonce,
|
||||||
|
license_id: licenseId,
|
||||||
|
new_domain: newDomain
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$message.text(response.data.message)
|
||||||
|
.removeClass('error').addClass('success').show();
|
||||||
|
|
||||||
|
// Update the domain display in the license card
|
||||||
|
var $domainDisplay = $('.license-domain-display[data-license-id="' + licenseId + '"]');
|
||||||
|
$domainDisplay.find('.domain-value').text(response.data.new_domain);
|
||||||
|
$domainDisplay.find('.wclp-transfer-btn').data('current-domain', response.data.new_domain);
|
||||||
|
|
||||||
|
// Close modal after a short delay
|
||||||
|
setTimeout(function() {
|
||||||
|
self.closeTransferModal();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
$message.text(response.data.message || wcLicensedProduct.strings.transferError)
|
||||||
|
.removeClass('success').addClass('error').show();
|
||||||
|
$submit.prop('disabled', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
var message = wcLicensedProduct.strings.transferError;
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
|
||||||
|
message = xhr.responseJSON.data.message;
|
||||||
|
}
|
||||||
|
$message.text(message)
|
||||||
|
.removeClass('success').addClass('error').show();
|
||||||
|
$submit.prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
538
openapi.json
Normal file
538
openapi.json
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"contact": {
|
||||||
|
"name": "Marco Graetsch",
|
||||||
|
"url": "https://src.bundespruefstelle.ch/magdev",
|
||||||
|
"email": "magdev3.0@gmail.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "GPL-2.0-or-later",
|
||||||
|
"url": "https://www.gnu.org/licenses/gpl-2.0.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "{baseUrl}/wp-json/wc-licensed-product/v1",
|
||||||
|
"description": "WordPress REST API endpoint",
|
||||||
|
"variables": {
|
||||||
|
"baseUrl": {
|
||||||
|
"default": "https://example.com",
|
||||||
|
"description": "The base URL of your WordPress installation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/validate": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "validateLicense",
|
||||||
|
"summary": "Validate a license key for a domain",
|
||||||
|
"description": "Validates whether a license key is valid for a specific domain. Checks license status, expiration, and domain binding. This is the primary endpoint for software to verify license validity.",
|
||||||
|
"tags": ["License Validation"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateRequest"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"license_key": "ABCD-1234-EFGH-5678",
|
||||||
|
"domain": "example.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "License is valid for the specified domain",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateSuccessResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"valid": true,
|
||||||
|
"license": {
|
||||||
|
"product_id": 123,
|
||||||
|
"expires_at": "2027-01-21",
|
||||||
|
"version_id": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "License validation failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ValidateErrorResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"license_not_found": {
|
||||||
|
"summary": "License key not found",
|
||||||
|
"value": {
|
||||||
|
"valid": false,
|
||||||
|
"error": "license_not_found",
|
||||||
|
"message": "License key not found."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license_revoked": {
|
||||||
|
"summary": "License has been revoked",
|
||||||
|
"value": {
|
||||||
|
"valid": false,
|
||||||
|
"error": "license_revoked",
|
||||||
|
"message": "This license has been revoked."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license_expired": {
|
||||||
|
"summary": "License has expired",
|
||||||
|
"value": {
|
||||||
|
"valid": false,
|
||||||
|
"error": "license_expired",
|
||||||
|
"message": "This license has expired."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license_inactive": {
|
||||||
|
"summary": "License is inactive",
|
||||||
|
"value": {
|
||||||
|
"valid": false,
|
||||||
|
"error": "license_inactive",
|
||||||
|
"message": "This license is inactive."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain_mismatch": {
|
||||||
|
"summary": "License not valid for this domain",
|
||||||
|
"value": {
|
||||||
|
"valid": false,
|
||||||
|
"error": "domain_mismatch",
|
||||||
|
"message": "This license is not valid for this domain."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"$ref": "#/components/responses/RateLimitExceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/status": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "checkStatus",
|
||||||
|
"summary": "Get license status information",
|
||||||
|
"description": "Retrieves detailed status information for a license key, including validity, domain binding, expiration date, and activation counts.",
|
||||||
|
"tags": ["License Status"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StatusRequest"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"license_key": "ABCD-1234-EFGH-5678"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StatusRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "License status retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/StatusResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"valid": true,
|
||||||
|
"status": "active",
|
||||||
|
"domain": "example.com",
|
||||||
|
"expires_at": "2027-01-21",
|
||||||
|
"activations_count": 1,
|
||||||
|
"max_activations": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "License key not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"valid": false,
|
||||||
|
"error": "license_not_found",
|
||||||
|
"message": "License key not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"$ref": "#/components/responses/RateLimitExceeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/activate": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "activateLicense",
|
||||||
|
"summary": "Activate a license on a domain",
|
||||||
|
"description": "Activates a license key on a specific domain. If the license is already activated on the same domain, returns success. If activating on a new domain, the old domain binding is replaced (single-domain licenses).",
|
||||||
|
"tags": ["License Activation"],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ActivateRequest"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"license_key": "ABCD-1234-EFGH-5678",
|
||||||
|
"domain": "newdomain.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ActivateRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "License activated successfully or already activated",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ActivateSuccessResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"activated": {
|
||||||
|
"summary": "License activated on new domain",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"message": "License activated successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"already_activated": {
|
||||||
|
"summary": "License already activated on this domain",
|
||||||
|
"value": {
|
||||||
|
"success": true,
|
||||||
|
"message": "License is already activated for this domain."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Activation failed due to license restrictions",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"license_invalid": {
|
||||||
|
"summary": "License is not valid",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"error": "license_invalid",
|
||||||
|
"message": "This license is not valid."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"max_activations_reached": {
|
||||||
|
"summary": "Maximum activations reached",
|
||||||
|
"value": {
|
||||||
|
"success": false,
|
||||||
|
"error": "max_activations_reached",
|
||||||
|
"message": "Maximum number of activations reached."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "License key not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"success": false,
|
||||||
|
"error": "license_not_found",
|
||||||
|
"message": "License key not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"$ref": "#/components/responses/RateLimitExceeded"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Server error during activation",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"success": false,
|
||||||
|
"error": "activation_failed",
|
||||||
|
"message": "Failed to activate license."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ValidateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["license_key", "domain"],
|
||||||
|
"properties": {
|
||||||
|
"license_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The license key to validate (format: XXXX-XXXX-XXXX-XXXX)",
|
||||||
|
"maxLength": 64,
|
||||||
|
"example": "ABCD-1234-EFGH-5678"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The domain to validate the license against",
|
||||||
|
"maxLength": 255,
|
||||||
|
"example": "example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ValidateSuccessResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": true,
|
||||||
|
"description": "Indicates the license is valid"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"product_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "WooCommerce product ID associated with the license"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "date",
|
||||||
|
"description": "Expiration date (null for lifetime licenses)"
|
||||||
|
},
|
||||||
|
"version_id": {
|
||||||
|
"type": ["integer", "null"],
|
||||||
|
"description": "Product version ID if license is bound to a version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ValidateErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": false,
|
||||||
|
"description": "Indicates validation failed"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["license_not_found", "license_revoked", "license_expired", "license_inactive", "domain_mismatch"],
|
||||||
|
"description": "Error code for programmatic handling"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable error message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StatusRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["license_key"],
|
||||||
|
"properties": {
|
||||||
|
"license_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The license key to check",
|
||||||
|
"example": "ABCD-1234-EFGH-5678"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StatusResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the license is currently valid"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["active", "inactive", "expired", "revoked"],
|
||||||
|
"description": "Current license status"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Domain the license is bound to"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"format": "date",
|
||||||
|
"description": "Expiration date (null for lifetime licenses)"
|
||||||
|
},
|
||||||
|
"activations_count": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Current number of activations"
|
||||||
|
},
|
||||||
|
"max_activations": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum allowed activations"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ActivateRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["license_key", "domain"],
|
||||||
|
"properties": {
|
||||||
|
"license_key": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The license key to activate",
|
||||||
|
"example": "ABCD-1234-EFGH-5678"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The domain to activate the license on",
|
||||||
|
"example": "newdomain.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ActivateSuccessResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": true,
|
||||||
|
"description": "Indicates activation was successful"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable success message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": false,
|
||||||
|
"description": "Indicates the operation failed"
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": false,
|
||||||
|
"description": "Indicates validation failed (for validation endpoints)"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Error code for programmatic handling"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Human-readable error message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RateLimitResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean",
|
||||||
|
"const": false
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "rate_limit_exceeded"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Too many requests. Please try again later."
|
||||||
|
},
|
||||||
|
"retry_after": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Seconds until rate limit resets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"RateLimitExceeded": {
|
||||||
|
"description": "Rate limit exceeded (30 requests per minute per IP)",
|
||||||
|
"headers": {
|
||||||
|
"Retry-After": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "Seconds until the rate limit resets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RateLimitResponse"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"success": false,
|
||||||
|
"error": "rate_limit_exceeded",
|
||||||
|
"message": "Too many requests. Please try again later.",
|
||||||
|
"retry_after": 45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "License Validation",
|
||||||
|
"description": "Validate license keys against domains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "License Status",
|
||||||
|
"description": "Check license status and details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "License Activation",
|
||||||
|
"description": "Activate licenses on domains"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
142
src/Admin/SettingsController.php
Normal file
142
src/Admin/SettingsController.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Settings Controller
|
||||||
|
*
|
||||||
|
* @package Jeremias\WcLicensedProduct\Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Jeremias\WcLicensedProduct\Admin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles WooCommerce settings tab for license defaults
|
||||||
|
*/
|
||||||
|
final class SettingsController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Settings option name
|
||||||
|
*/
|
||||||
|
public const OPTION_NAME = 'wc_licensed_product_settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->registerHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register WordPress hooks
|
||||||
|
*/
|
||||||
|
private function registerHooks(): void
|
||||||
|
{
|
||||||
|
add_filter('woocommerce_settings_tabs_array', [$this, 'addSettingsTab'], 50);
|
||||||
|
add_action('woocommerce_settings_tabs_licensed_product', [$this, 'renderSettingsTab']);
|
||||||
|
add_action('woocommerce_update_options_licensed_product', [$this, 'saveSettings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add settings tab to WooCommerce settings
|
||||||
|
*/
|
||||||
|
public function addSettingsTab(array $tabs): array
|
||||||
|
{
|
||||||
|
$tabs['licensed_product'] = __('Licensed Products', 'wc-licensed-product');
|
||||||
|
return $tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings fields
|
||||||
|
*/
|
||||||
|
public function getSettingsFields(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'section_title' => [
|
||||||
|
'name' => __('Default License Settings', 'wc-licensed-product'),
|
||||||
|
'type' => 'title',
|
||||||
|
'desc' => __('These settings serve as defaults for new licensed products. Individual product settings override these defaults.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_section_defaults',
|
||||||
|
],
|
||||||
|
'default_max_activations' => [
|
||||||
|
'name' => __('Default Max Activations', 'wc-licensed-product'),
|
||||||
|
'type' => 'number',
|
||||||
|
'desc' => __('Default maximum number of domain activations per license.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_default_max_activations',
|
||||||
|
'default' => '1',
|
||||||
|
'custom_attributes' => [
|
||||||
|
'min' => '1',
|
||||||
|
'step' => '1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'default_validity_days' => [
|
||||||
|
'name' => __('Default License Validity (Days)', 'wc-licensed-product'),
|
||||||
|
'type' => 'number',
|
||||||
|
'desc' => __('Default number of days a license is valid. Leave empty or set to 0 for lifetime licenses.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_default_validity_days',
|
||||||
|
'default' => '',
|
||||||
|
'placeholder' => __('Lifetime', 'wc-licensed-product'),
|
||||||
|
'custom_attributes' => [
|
||||||
|
'min' => '0',
|
||||||
|
'step' => '1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'default_bind_to_version' => [
|
||||||
|
'name' => __('Default Bind to Major Version', 'wc-licensed-product'),
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'desc' => __('If enabled, licenses are bound to the major version at purchase time by default.', 'wc-licensed-product'),
|
||||||
|
'id' => 'wc_licensed_product_default_bind_to_version',
|
||||||
|
'default' => 'no',
|
||||||
|
],
|
||||||
|
'section_end' => [
|
||||||
|
'type' => 'sectionend',
|
||||||
|
'id' => 'wc_licensed_product_section_defaults_end',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render settings tab content
|
||||||
|
*/
|
||||||
|
public function renderSettingsTab(): void
|
||||||
|
{
|
||||||
|
woocommerce_admin_fields($this->getSettingsFields());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save settings
|
||||||
|
*/
|
||||||
|
public function saveSettings(): void
|
||||||
|
{
|
||||||
|
woocommerce_update_options($this->getSettingsFields());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default max activations
|
||||||
|
*/
|
||||||
|
public static function getDefaultMaxActivations(): int
|
||||||
|
{
|
||||||
|
$value = get_option('wc_licensed_product_default_max_activations', 1);
|
||||||
|
return max(1, (int) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default validity days
|
||||||
|
*/
|
||||||
|
public static function getDefaultValidityDays(): ?int
|
||||||
|
{
|
||||||
|
$value = get_option('wc_licensed_product_default_validity_days', '');
|
||||||
|
if ($value === '' || $value === '0') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default bind to version setting
|
||||||
|
*/
|
||||||
|
public static function getDefaultBindToVersion(): bool
|
||||||
|
{
|
||||||
|
return get_option('wc_licensed_product_default_bind_to_version', 'no') === 'yes';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,25 +180,6 @@ final class RestApiController
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Deactivate license endpoint (public)
|
|
||||||
register_rest_route(self::NAMESPACE, '/deactivate', [
|
|
||||||
'methods' => WP_REST_Server::CREATABLE,
|
|
||||||
'callback' => [$this, 'deactivateLicense'],
|
|
||||||
'permission_callback' => '__return_true',
|
|
||||||
'args' => [
|
|
||||||
'license_key' => [
|
|
||||||
'required' => true,
|
|
||||||
'type' => 'string',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
],
|
|
||||||
'domain' => [
|
|
||||||
'required' => true,
|
|
||||||
'type' => 'string',
|
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -318,55 +299,4 @@ final class RestApiController
|
|||||||
'message' => __('License activated successfully.', 'wc-licensed-product'),
|
'message' => __('License activated successfully.', 'wc-licensed-product'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deactivate license endpoint
|
|
||||||
*/
|
|
||||||
public function deactivateLicense(WP_REST_Request $request): WP_REST_Response
|
|
||||||
{
|
|
||||||
$rateLimitResponse = $this->checkRateLimit();
|
|
||||||
if ($rateLimitResponse !== null) {
|
|
||||||
return $rateLimitResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
$licenseKey = $request->get_param('license_key');
|
|
||||||
$domain = $request->get_param('domain');
|
|
||||||
|
|
||||||
$license = $this->licenseManager->getLicenseByKey($licenseKey);
|
|
||||||
|
|
||||||
if (!$license) {
|
|
||||||
return new WP_REST_Response([
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'license_not_found',
|
|
||||||
'message' => __('License key not found.', 'wc-licensed-product'),
|
|
||||||
], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
|
||||||
|
|
||||||
// Verify domain matches
|
|
||||||
if ($license->getDomain() !== $normalizedDomain) {
|
|
||||||
return new WP_REST_Response([
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'domain_mismatch',
|
|
||||||
'message' => __('License is not activated for this domain.', 'wc-licensed-product'),
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set status to inactive
|
|
||||||
$success = $this->licenseManager->updateLicenseStatus($license->getId(), 'inactive');
|
|
||||||
|
|
||||||
if (!$success) {
|
|
||||||
return new WP_REST_Response([
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'deactivation_failed',
|
|
||||||
'message' => __('Failed to deactivate license.', 'wc-licensed-product'),
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WP_REST_Response([
|
|
||||||
'success' => true,
|
|
||||||
'message' => __('License deactivated successfully.', 'wc-licensed-product'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,210 @@ final class LicenseEmailController
|
|||||||
|
|
||||||
// Add license info to order details in emails
|
// Add license info to order details in emails
|
||||||
add_action('woocommerce_order_item_meta_end', [$this, 'addLicenseToOrderItem'], 10, 4);
|
add_action('woocommerce_order_item_meta_end', [$this, 'addLicenseToOrderItem'], 10, 4);
|
||||||
|
|
||||||
|
// Schedule cron job for expiration warnings
|
||||||
|
add_action('init', [$this, 'scheduleExpirationCheck']);
|
||||||
|
|
||||||
|
// Cron action for checking expiring licenses
|
||||||
|
add_action('wclp_check_expiring_licenses', [$this, 'sendExpirationWarnings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule the expiration check cron job
|
||||||
|
*/
|
||||||
|
public function scheduleExpirationCheck(): void
|
||||||
|
{
|
||||||
|
if (!wp_next_scheduled('wclp_check_expiring_licenses')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'wclp_check_expiring_licenses');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send expiration warning emails
|
||||||
|
*/
|
||||||
|
public function sendExpirationWarnings(): void
|
||||||
|
{
|
||||||
|
// Check for licenses expiring in 7 days
|
||||||
|
$this->processExpirationWarnings(7, 'expiring_7_days');
|
||||||
|
|
||||||
|
// Check for licenses expiring in 1 day
|
||||||
|
$this->processExpirationWarnings(1, 'expiring_1_day');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process and send expiration warnings for a specific time frame
|
||||||
|
*
|
||||||
|
* @param int $days Days until expiration
|
||||||
|
* @param string $notificationType Notification type identifier
|
||||||
|
*/
|
||||||
|
private function processExpirationWarnings(int $days, string $notificationType): void
|
||||||
|
{
|
||||||
|
$licenses = $this->licenseManager->getLicensesExpiringSoon($days);
|
||||||
|
|
||||||
|
foreach ($licenses as $license) {
|
||||||
|
// Skip if already notified
|
||||||
|
if ($this->licenseManager->wasExpirationNotified($license->getId(), $notificationType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the warning email
|
||||||
|
if ($this->sendExpirationWarningEmail($license, $days)) {
|
||||||
|
// Mark as notified
|
||||||
|
$this->licenseManager->markExpirationNotified($license->getId(), $notificationType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send expiration warning email to customer
|
||||||
|
*
|
||||||
|
* @param \Jeremias\WcLicensedProduct\License\License $license License object
|
||||||
|
* @param int $daysRemaining Days until expiration
|
||||||
|
* @return bool Whether email was sent successfully
|
||||||
|
*/
|
||||||
|
private function sendExpirationWarningEmail($license, int $daysRemaining): bool
|
||||||
|
{
|
||||||
|
$customer = get_userdata($license->getCustomerId());
|
||||||
|
if (!$customer || !$customer->user_email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product($license->getProductId());
|
||||||
|
$productName = $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product');
|
||||||
|
|
||||||
|
$siteName = get_bloginfo('name');
|
||||||
|
$expiresAt = $license->getExpiresAt();
|
||||||
|
$expirationDate = $expiresAt ? $expiresAt->format(get_option('date_format')) : '';
|
||||||
|
|
||||||
|
// Email subject
|
||||||
|
if ($daysRemaining === 1) {
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Product name, 2: Site name */
|
||||||
|
__('[%2$s] Your license for %1$s expires tomorrow', 'wc-licensed-product'),
|
||||||
|
$productName,
|
||||||
|
$siteName
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$subject = sprintf(
|
||||||
|
/* translators: 1: Product name, 2: Number of days, 3: Site name */
|
||||||
|
__('[%3$s] Your license for %1$s expires in %2$d days', 'wc-licensed-product'),
|
||||||
|
$productName,
|
||||||
|
$daysRemaining,
|
||||||
|
$siteName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email content
|
||||||
|
$message = $this->buildExpirationWarningHtml($license, $customer, $productName, $daysRemaining, $expirationDate);
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: text/html; charset=UTF-8',
|
||||||
|
'From: ' . $siteName . ' <' . get_option('admin_email') . '>',
|
||||||
|
];
|
||||||
|
|
||||||
|
return wp_mail($customer->user_email, $subject, $message, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build HTML content for expiration warning email
|
||||||
|
*
|
||||||
|
* @param \Jeremias\WcLicensedProduct\License\License $license License object
|
||||||
|
* @param \WP_User $customer Customer user object
|
||||||
|
* @param string $productName Product name
|
||||||
|
* @param int $daysRemaining Days until expiration
|
||||||
|
* @param string $expirationDate Formatted expiration date
|
||||||
|
* @return string HTML email content
|
||||||
|
*/
|
||||||
|
private function buildExpirationWarningHtml($license, $customer, string $productName, int $daysRemaining, string $expirationDate): string
|
||||||
|
{
|
||||||
|
$siteName = get_bloginfo('name');
|
||||||
|
$siteUrl = home_url();
|
||||||
|
$accountUrl = wc_get_account_endpoint_url('licenses');
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||||
|
<div style="background: #f8f9fa; padding: 30px; border-radius: 8px;">
|
||||||
|
<h1 style="color: #333; margin-top: 0; font-size: 24px;">
|
||||||
|
<?php esc_html_e('License Expiration Notice', 'wc-licensed-product'); ?>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p><?php printf(esc_html__('Hello %s,', 'wc-licensed-product'), esc_html($customer->display_name)); ?></p>
|
||||||
|
|
||||||
|
<?php if ($daysRemaining === 1): ?>
|
||||||
|
<p style="color: #dc3545; font-weight: 600;">
|
||||||
|
<?php printf(
|
||||||
|
esc_html__('Your license for %s will expire tomorrow (%s).', 'wc-licensed-product'),
|
||||||
|
esc_html($productName),
|
||||||
|
esc_html($expirationDate)
|
||||||
|
); ?>
|
||||||
|
</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p style="color: #ffc107; font-weight: 600;">
|
||||||
|
<?php printf(
|
||||||
|
esc_html__('Your license for %1$s will expire in %2$d days (%3$s).', 'wc-licensed-product'),
|
||||||
|
esc_html($productName),
|
||||||
|
$daysRemaining,
|
||||||
|
esc_html($expirationDate)
|
||||||
|
); ?>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div style="background: #fff; padding: 20px; border-radius: 4px; margin: 20px 0; border: 1px solid #e5e5e5;">
|
||||||
|
<h3 style="margin-top: 0; font-size: 16px;"><?php esc_html_e('License Details', 'wc-licensed-product'); ?></h3>
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('Product:', 'wc-licensed-product'); ?></td>
|
||||||
|
<td style="padding: 8px 0; font-weight: 600;"><?php echo esc_html($productName); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('License Key:', 'wc-licensed-product'); ?></td>
|
||||||
|
<td style="padding: 8px 0;">
|
||||||
|
<code style="background: #f5f5f5; padding: 3px 8px; border-radius: 3px; font-family: monospace;">
|
||||||
|
<?php echo esc_html($license->getLicenseKey()); ?>
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></td>
|
||||||
|
<td style="padding: 8px 0;"><?php echo esc_html($license->getDomain()); ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666;"><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></td>
|
||||||
|
<td style="padding: 8px 0; color: #dc3545; font-weight: 600;"><?php echo esc_html($expirationDate); ?></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><?php esc_html_e('To continue using this product, please renew your license before the expiration date.', 'wc-licensed-product'); ?></p>
|
||||||
|
|
||||||
|
<p style="margin-top: 25px;">
|
||||||
|
<a href="<?php echo esc_url($accountUrl); ?>"
|
||||||
|
style="display: inline-block; background: #2271b1; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: 600;">
|
||||||
|
<?php esc_html_e('View My Licenses', 'wc-licensed-product'); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style="border: none; border-top: 1px solid #e5e5e5; margin: 30px 0;">
|
||||||
|
|
||||||
|
<p style="font-size: 14px; color: #666; margin-bottom: 0;">
|
||||||
|
<?php printf(
|
||||||
|
esc_html__('This email was sent from %s.', 'wc-licensed-product'),
|
||||||
|
'<a href="' . esc_url($siteUrl) . '" style="color: #2271b1;">' . esc_html($siteName) . '</a>'
|
||||||
|
); ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ final class AccountController
|
|||||||
|
|
||||||
// Enqueue frontend styles and scripts
|
// Enqueue frontend styles and scripts
|
||||||
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
|
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||||
|
|
||||||
|
// AJAX handler for license transfer request
|
||||||
|
add_action('wp_ajax_wclp_customer_transfer_license', [$this, 'handleTransferRequest']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +184,19 @@ final class AccountController
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="license-info-row">
|
<div class="license-info-row">
|
||||||
<span><strong><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></strong> <?php echo esc_html($item['license']->getDomain()); ?></span>
|
<span class="license-domain-display" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
|
||||||
|
<strong><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></strong>
|
||||||
|
<span class="domain-value"><?php echo esc_html($item['license']->getDomain()); ?></span>
|
||||||
|
<?php if (in_array($item['license']->getStatus(), ['active', 'inactive'], true)): ?>
|
||||||
|
<button type="button" class="wclp-transfer-btn"
|
||||||
|
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'); ?>">
|
||||||
|
<span class="dashicons dashicons-randomize"></span>
|
||||||
|
<?php esc_html_e('Transfer', 'wc-licensed-product'); ?>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
<span><strong><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></strong>
|
<span><strong><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></strong>
|
||||||
<?php
|
<?php
|
||||||
$expiresAt = $item['license']->getExpiresAt();
|
$expiresAt = $item['license']->getExpiresAt();
|
||||||
@@ -213,6 +228,40 @@ final class AccountController
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfer Modal -->
|
||||||
|
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
|
||||||
|
<div class="wclp-modal-overlay"></div>
|
||||||
|
<div class="wclp-modal-content">
|
||||||
|
<button type="button" class="wclp-modal-close" aria-label="<?php esc_attr_e('Close', 'wc-licensed-product'); ?>">×</button>
|
||||||
|
<h3><?php esc_html_e('Transfer License to New Domain', 'wc-licensed-product'); ?></h3>
|
||||||
|
<form id="wclp-transfer-form">
|
||||||
|
<input type="hidden" name="license_id" id="transfer-license-id" value="">
|
||||||
|
|
||||||
|
<div class="wclp-form-row">
|
||||||
|
<label><?php esc_html_e('Current Domain', 'wc-licensed-product'); ?></label>
|
||||||
|
<p class="wclp-current-domain"><code id="transfer-current-domain"></code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-form-row">
|
||||||
|
<label for="transfer-new-domain"><?php esc_html_e('New Domain', 'wc-licensed-product'); ?></label>
|
||||||
|
<input type="text" name="new_domain" id="transfer-new-domain"
|
||||||
|
placeholder="example.com" required
|
||||||
|
pattern="[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+">
|
||||||
|
<p class="wclp-field-description"><?php esc_html_e('Enter the new domain without http:// or www.', 'wc-licensed-product'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-form-row wclp-form-actions">
|
||||||
|
<button type="submit" class="button wclp-btn-primary" id="wclp-transfer-submit">
|
||||||
|
<?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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wclp-transfer-message" class="wclp-message" style="display:none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +290,111 @@ final class AccountController
|
|||||||
);
|
);
|
||||||
|
|
||||||
wp_localize_script('wc-licensed-product-frontend', 'wcLicensedProduct', [
|
wp_localize_script('wc-licensed-product-frontend', 'wcLicensedProduct', [
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
'transferNonce' => wp_create_nonce('wclp_customer_transfer'),
|
||||||
'strings' => [
|
'strings' => [
|
||||||
'copied' => __('Copied!', 'wc-licensed-product'),
|
'copied' => __('Copied!', 'wc-licensed-product'),
|
||||||
'copyFailed' => __('Copy failed', 'wc-licensed-product'),
|
'copyFailed' => __('Copy failed', 'wc-licensed-product'),
|
||||||
|
'transferSuccess' => __('License transferred successfully!', 'wc-licensed-product'),
|
||||||
|
'transferError' => __('Transfer failed. Please try again.', 'wc-licensed-product'),
|
||||||
|
'transferConfirm' => __('Are you sure you want to transfer this license to a new domain? This action cannot be undone.', 'wc-licensed-product'),
|
||||||
|
'invalidDomain' => __('Please enter a valid domain.', 'wc-licensed-product'),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle AJAX license transfer request from customer
|
||||||
|
*/
|
||||||
|
public function handleTransferRequest(): void
|
||||||
|
{
|
||||||
|
// Verify nonce
|
||||||
|
if (!check_ajax_referer('wclp_customer_transfer', 'nonce', false)) {
|
||||||
|
wp_send_json_error(['message' => __('Security check failed.', 'wc-licensed-product')], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user is logged in
|
||||||
|
$customerId = get_current_user_id();
|
||||||
|
if (!$customerId) {
|
||||||
|
wp_send_json_error(['message' => __('Please log in to transfer a license.', 'wc-licensed-product')], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate license ID
|
||||||
|
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
|
||||||
|
if (!$licenseId) {
|
||||||
|
wp_send_json_error(['message' => __('Invalid license.', 'wc-licensed-product')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and validate new domain
|
||||||
|
$newDomain = isset($_POST['new_domain']) ? sanitize_text_field($_POST['new_domain']) : '';
|
||||||
|
$newDomain = $this->normalizeDomain($newDomain);
|
||||||
|
|
||||||
|
if (empty($newDomain)) {
|
||||||
|
wp_send_json_error(['message' => __('Please enter a valid domain.', 'wc-licensed-product')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the license belongs to this customer
|
||||||
|
$license = $this->licenseManager->getLicenseById($licenseId);
|
||||||
|
if (!$license) {
|
||||||
|
wp_send_json_error(['message' => __('License not found.', 'wc-licensed-product')], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($license->getCustomerId() !== $customerId) {
|
||||||
|
wp_send_json_error(['message' => __('You do not have permission to transfer this license.', 'wc-licensed-product')], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if license is in a transferable state
|
||||||
|
if ($license->getStatus() === 'revoked') {
|
||||||
|
wp_send_json_error(['message' => __('Revoked licenses cannot be transferred.', 'wc-licensed-product')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($license->getStatus() === 'expired') {
|
||||||
|
wp_send_json_error(['message' => __('Expired licenses cannot be transferred.', 'wc-licensed-product')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if domain is the same
|
||||||
|
if ($license->getDomain() === $newDomain) {
|
||||||
|
wp_send_json_error(['message' => __('The new domain is the same as the current domain.', 'wc-licensed-product')], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the transfer
|
||||||
|
$result = $this->licenseManager->transferLicense($licenseId, $newDomain);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success([
|
||||||
|
'message' => __('License transferred successfully!', 'wc-licensed-product'),
|
||||||
|
'new_domain' => $newDomain,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
wp_send_json_error(['message' => __('Failed to transfer license. Please try again.', 'wc-licensed-product')], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize domain for comparison and storage
|
||||||
|
*/
|
||||||
|
private function normalizeDomain(string $domain): string
|
||||||
|
{
|
||||||
|
// Remove protocol if present
|
||||||
|
$domain = preg_replace('#^https?://#i', '', $domain);
|
||||||
|
|
||||||
|
// Remove www prefix
|
||||||
|
$domain = preg_replace('#^www\.#i', '', $domain);
|
||||||
|
|
||||||
|
// Remove trailing slash
|
||||||
|
$domain = rtrim($domain, '/');
|
||||||
|
|
||||||
|
// Remove path if present
|
||||||
|
$domain = explode('/', $domain)[0];
|
||||||
|
|
||||||
|
// Convert to lowercase
|
||||||
|
$domain = strtolower($domain);
|
||||||
|
|
||||||
|
// Basic validation - must contain at least one dot and valid characters
|
||||||
|
if (!preg_match('/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/', $domain)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $domain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ final class Installer
|
|||||||
*/
|
*/
|
||||||
public static function deactivate(): void
|
public static function deactivate(): void
|
||||||
{
|
{
|
||||||
|
// Clear scheduled cron events
|
||||||
|
$timestamp = wp_next_scheduled('wclp_check_expiring_licenses');
|
||||||
|
if ($timestamp) {
|
||||||
|
wp_unschedule_event($timestamp, 'wclp_check_expiring_licenses');
|
||||||
|
}
|
||||||
|
|
||||||
// Flush rewrite rules
|
// Flush rewrite rules
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,21 +176,63 @@ class LicenseManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all licenses (for admin)
|
* Get all licenses (for admin) with optional filtering
|
||||||
|
*
|
||||||
|
* @param int $page Page number
|
||||||
|
* @param int $perPage Items per page
|
||||||
|
* @param array $filters Optional filters: search, status, product_id, customer_id
|
||||||
|
* @return array Array of License objects
|
||||||
*/
|
*/
|
||||||
public function getAllLicenses(int $page = 1, int $perPage = 20): array
|
public function getAllLicenses(int $page = 1, int $perPage = 20, array $filters = []): array
|
||||||
{
|
{
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$tableName = Installer::getLicensesTable();
|
$tableName = Installer::getLicensesTable();
|
||||||
$offset = ($page - 1) * $perPage;
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Search filter (searches license key, domain, customer email)
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$search = '%' . $wpdb->esc_like($filters['search']) . '%';
|
||||||
|
$where[] = "(license_key LIKE %s OR domain LIKE %s)";
|
||||||
|
$params[] = $search;
|
||||||
|
$params[] = $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (!empty($filters['status']) && in_array($filters['status'], [
|
||||||
|
License::STATUS_ACTIVE,
|
||||||
|
License::STATUS_INACTIVE,
|
||||||
|
License::STATUS_EXPIRED,
|
||||||
|
License::STATUS_REVOKED,
|
||||||
|
], true)) {
|
||||||
|
$where[] = "status = %s";
|
||||||
|
$params[] = $filters['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product filter
|
||||||
|
if (!empty($filters['product_id'])) {
|
||||||
|
$where[] = "product_id = %d";
|
||||||
|
$params[] = absint($filters['product_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer filter
|
||||||
|
if (!empty($filters['customer_id'])) {
|
||||||
|
$where[] = "customer_id = %d";
|
||||||
|
$params[] = absint($filters['customer_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
$params[] = $perPage;
|
||||||
|
$params[] = $offset;
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM {$tableName} {$whereClause} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||||
|
|
||||||
$rows = $wpdb->get_results(
|
$rows = $wpdb->get_results(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare($sql, $params),
|
||||||
"SELECT * FROM {$tableName} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
|
||||||
$perPage,
|
|
||||||
$offset
|
|
||||||
),
|
|
||||||
ARRAY_A
|
ARRAY_A
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -198,16 +240,85 @@ class LicenseManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get total license count
|
* Get total license count with optional filtering
|
||||||
|
*
|
||||||
|
* @param array $filters Optional filters: search, status, product_id, customer_id
|
||||||
|
* @return int Total count
|
||||||
*/
|
*/
|
||||||
public function getLicenseCount(): int
|
public function getLicenseCount(array $filters = []): int
|
||||||
{
|
{
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$tableName = Installer::getLicensesTable();
|
$tableName = Installer::getLicensesTable();
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$search = '%' . $wpdb->esc_like($filters['search']) . '%';
|
||||||
|
$where[] = "(license_key LIKE %s OR domain LIKE %s)";
|
||||||
|
$params[] = $search;
|
||||||
|
$params[] = $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (!empty($filters['status']) && in_array($filters['status'], [
|
||||||
|
License::STATUS_ACTIVE,
|
||||||
|
License::STATUS_INACTIVE,
|
||||||
|
License::STATUS_EXPIRED,
|
||||||
|
License::STATUS_REVOKED,
|
||||||
|
], true)) {
|
||||||
|
$where[] = "status = %s";
|
||||||
|
$params[] = $filters['status'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product filter
|
||||||
|
if (!empty($filters['product_id'])) {
|
||||||
|
$where[] = "product_id = %d";
|
||||||
|
$params[] = absint($filters['product_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customer filter
|
||||||
|
if (!empty($filters['customer_id'])) {
|
||||||
|
$where[] = "customer_id = %d";
|
||||||
|
$params[] = absint($filters['customer_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
|
if (empty($params)) {
|
||||||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare("SELECT COUNT(*) FROM {$tableName} {$whereClause}", $params)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all licensed products for filter dropdown
|
||||||
|
*
|
||||||
|
* @return array Array of [id => name] pairs
|
||||||
|
*/
|
||||||
|
public function getLicensedProducts(): array
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$productIds = $wpdb->get_col("SELECT DISTINCT product_id FROM {$tableName}");
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
foreach ($productIds as $productId) {
|
||||||
|
$product = wc_get_product((int) $productId);
|
||||||
|
if ($product) {
|
||||||
|
$products[(int) $productId] = $product->get_name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a license key for a domain
|
* Validate a license key for a domain
|
||||||
*/
|
*/
|
||||||
@@ -360,4 +471,430 @@ class LicenseManager
|
|||||||
|
|
||||||
return $versionId ? (int) $versionId : null;
|
return $versionId ? (int) $versionId : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend license expiration
|
||||||
|
*
|
||||||
|
* @param int $licenseId License ID
|
||||||
|
* @param int $days Number of days to extend
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public function extendLicense(int $licenseId, int $days): bool
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$license = $this->getLicenseById($licenseId);
|
||||||
|
if (!$license) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new expiration date
|
||||||
|
$currentExpiry = $license->getExpiresAt();
|
||||||
|
if ($currentExpiry === null) {
|
||||||
|
// License is lifetime, set expiration from now
|
||||||
|
$newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
|
||||||
|
} elseif ($currentExpiry < new \DateTimeImmutable()) {
|
||||||
|
// License is expired, extend from now
|
||||||
|
$newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
|
||||||
|
} else {
|
||||||
|
// License still valid, extend from current expiry
|
||||||
|
$newExpiry = \DateTimeImmutable::createFromInterface($currentExpiry)->modify("+{$days} days");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$result = $wpdb->update(
|
||||||
|
$tableName,
|
||||||
|
['expires_at' => $newExpiry->format('Y-m-d H:i:s')],
|
||||||
|
['id' => $licenseId],
|
||||||
|
['%s'],
|
||||||
|
['%d']
|
||||||
|
);
|
||||||
|
|
||||||
|
// If license was expired, reactivate it
|
||||||
|
if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED) {
|
||||||
|
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set license to lifetime (no expiration)
|
||||||
|
*
|
||||||
|
* @param int $licenseId License ID
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public function setLicenseLifetime(int $licenseId): bool
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$license = $this->getLicenseById($licenseId);
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
// Use raw query to set NULL
|
||||||
|
$result = $wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"UPDATE {$tableName} SET expires_at = NULL WHERE id = %d",
|
||||||
|
$licenseId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If license was expired, reactivate it
|
||||||
|
if ($result !== false && $license && $license->getStatus() === License::STATUS_EXPIRED) {
|
||||||
|
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk update license status
|
||||||
|
*
|
||||||
|
* @param array $licenseIds Array of license IDs
|
||||||
|
* @param string $status New status
|
||||||
|
* @return int Number of licenses updated
|
||||||
|
*/
|
||||||
|
public function bulkUpdateStatus(array $licenseIds, string $status): int
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (empty($licenseIds)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$ids = array_map('absint', $licenseIds);
|
||||||
|
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||||
|
|
||||||
|
$result = $wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"UPDATE {$tableName} SET status = %s WHERE id IN ({$placeholders})",
|
||||||
|
array_merge([$status], $ids)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false ? (int) $result : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk delete licenses
|
||||||
|
*
|
||||||
|
* @param array $licenseIds Array of license IDs
|
||||||
|
* @return int Number of licenses deleted
|
||||||
|
*/
|
||||||
|
public function bulkDelete(array $licenseIds): int
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (empty($licenseIds)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$ids = array_map('absint', $licenseIds);
|
||||||
|
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||||
|
|
||||||
|
$result = $wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$tableName} WHERE id IN ({$placeholders})",
|
||||||
|
$ids
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false ? (int) $result : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk extend licenses
|
||||||
|
*
|
||||||
|
* @param array $licenseIds Array of license IDs
|
||||||
|
* @param int $days Number of days to extend
|
||||||
|
* @return int Number of licenses extended
|
||||||
|
*/
|
||||||
|
public function bulkExtend(array $licenseIds, int $days): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
foreach ($licenseIds as $licenseId) {
|
||||||
|
if ($this->extendLicense((int) $licenseId, $days)) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfer license to a new domain
|
||||||
|
*
|
||||||
|
* @param int $licenseId License ID
|
||||||
|
* @param string $newDomain New domain to transfer to
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public function transferLicense(int $licenseId, string $newDomain): bool
|
||||||
|
{
|
||||||
|
$license = $this->getLicenseById($licenseId);
|
||||||
|
if (!$license) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot transfer revoked licenses
|
||||||
|
if ($license->getStatus() === License::STATUS_REVOKED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->updateLicenseDomain($licenseId, $newDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get license statistics
|
||||||
|
*
|
||||||
|
* @return array Statistics data
|
||||||
|
*/
|
||||||
|
public function getStatistics(): array
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
|
||||||
|
// Get counts by status
|
||||||
|
$statusCounts = $wpdb->get_results(
|
||||||
|
"SELECT status, COUNT(*) as count FROM {$tableName} GROUP BY status",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
$byStatus = [
|
||||||
|
License::STATUS_ACTIVE => 0,
|
||||||
|
License::STATUS_INACTIVE => 0,
|
||||||
|
License::STATUS_EXPIRED => 0,
|
||||||
|
License::STATUS_REVOKED => 0,
|
||||||
|
];
|
||||||
|
foreach ($statusCounts ?: [] as $row) {
|
||||||
|
$byStatus[$row['status']] = (int) $row['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
$total = array_sum($byStatus);
|
||||||
|
|
||||||
|
// Get lifetime vs expiring licenses
|
||||||
|
$lifetimeCount = (int) $wpdb->get_var(
|
||||||
|
"SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NULL"
|
||||||
|
);
|
||||||
|
$expiringCount = $total - $lifetimeCount;
|
||||||
|
|
||||||
|
// Get licenses expiring soon (next 30 days)
|
||||||
|
$expiringSoon = (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NOT NULL AND expires_at <= %s AND expires_at > NOW() AND status = %s",
|
||||||
|
(new \DateTimeImmutable())->modify('+30 days')->format('Y-m-d H:i:s'),
|
||||||
|
License::STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get licenses by product
|
||||||
|
$byProduct = $wpdb->get_results(
|
||||||
|
"SELECT product_id, COUNT(*) as count FROM {$tableName} GROUP BY product_id ORDER BY count DESC LIMIT 10",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
$productStats = [];
|
||||||
|
foreach ($byProduct ?: [] as $row) {
|
||||||
|
$product = wc_get_product((int) $row['product_id']);
|
||||||
|
$productStats[] = [
|
||||||
|
'product_id' => (int) $row['product_id'],
|
||||||
|
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
|
||||||
|
'count' => (int) $row['count'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get licenses created per month (last 12 months)
|
||||||
|
$monthlyData = $wpdb->get_results(
|
||||||
|
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count
|
||||||
|
FROM {$tableName}
|
||||||
|
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||||
|
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
|
||||||
|
ORDER BY month ASC",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
$monthlyStats = [];
|
||||||
|
foreach ($monthlyData ?: [] as $row) {
|
||||||
|
$monthlyStats[$row['month']] = (int) $row['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get top domains
|
||||||
|
$topDomains = $wpdb->get_results(
|
||||||
|
"SELECT domain, COUNT(*) as count FROM {$tableName} GROUP BY domain ORDER BY count DESC LIMIT 10",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $total,
|
||||||
|
'by_status' => $byStatus,
|
||||||
|
'lifetime' => $lifetimeCount,
|
||||||
|
'expiring' => $expiringCount,
|
||||||
|
'expiring_soon' => $expiringSoon,
|
||||||
|
'by_product' => $productStats,
|
||||||
|
'monthly' => $monthlyStats,
|
||||||
|
'top_domains' => $topDomains ?: [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get licenses expiring within specified days
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to look ahead
|
||||||
|
* @param bool $excludeNotified Whether to exclude already notified licenses
|
||||||
|
* @return array Array of License objects with customer data
|
||||||
|
*/
|
||||||
|
public function getLicensesExpiringSoon(int $days = 7, bool $excludeNotified = true): array
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$now = new \DateTimeImmutable();
|
||||||
|
$future = $now->modify("+{$days} days");
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM {$tableName}
|
||||||
|
WHERE expires_at IS NOT NULL
|
||||||
|
AND expires_at > %s
|
||||||
|
AND expires_at <= %s
|
||||||
|
AND status = %s";
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
$now->format('Y-m-d H:i:s'),
|
||||||
|
$future->format('Y-m-d H:i:s'),
|
||||||
|
License::STATUS_ACTIVE,
|
||||||
|
];
|
||||||
|
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
$wpdb->prepare($sql, $params),
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark license as notified for expiration warning
|
||||||
|
*
|
||||||
|
* @param int $licenseId License ID
|
||||||
|
* @param string $notificationType Type of notification (e.g., 'expiring_7_days', 'expiring_1_day')
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public function markExpirationNotified(int $licenseId, string $notificationType): bool
|
||||||
|
{
|
||||||
|
$metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType);
|
||||||
|
update_user_meta($this->getLicenseById($licenseId)?->getCustomerId() ?? 0, $metaKey . '_' . $licenseId, current_time('mysql'));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if license was already notified for expiration
|
||||||
|
*
|
||||||
|
* @param int $licenseId License ID
|
||||||
|
* @param string $notificationType Type of notification
|
||||||
|
* @return bool Whether already notified
|
||||||
|
*/
|
||||||
|
public function wasExpirationNotified(int $licenseId, string $notificationType): bool
|
||||||
|
{
|
||||||
|
$license = $this->getLicenseById($licenseId);
|
||||||
|
if (!$license) {
|
||||||
|
return true; // Consider notified if license doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType) . '_' . $licenseId;
|
||||||
|
return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a license from CSV data
|
||||||
|
*
|
||||||
|
* @param string $licenseKey License key
|
||||||
|
* @param int $productId Product ID
|
||||||
|
* @param int $customerId Customer ID
|
||||||
|
* @param string $domain Domain name
|
||||||
|
* @param int $orderId Order ID (optional)
|
||||||
|
* @param string $status License status
|
||||||
|
* @param int $maxActivations Maximum activations
|
||||||
|
* @param int $activationsCount Current activation count
|
||||||
|
* @param \DateTimeImmutable|null $expiresAt Expiration date or null for lifetime
|
||||||
|
* @return bool Success
|
||||||
|
*/
|
||||||
|
public function importLicense(
|
||||||
|
string $licenseKey,
|
||||||
|
int $productId,
|
||||||
|
int $customerId,
|
||||||
|
string $domain,
|
||||||
|
int $orderId = 0,
|
||||||
|
string $status = License::STATUS_ACTIVE,
|
||||||
|
int $maxActivations = 1,
|
||||||
|
int $activationsCount = 1,
|
||||||
|
?\DateTimeImmutable $expiresAt = null
|
||||||
|
): bool {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$tableName,
|
||||||
|
[
|
||||||
|
'license_key' => $licenseKey,
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'product_id' => $productId,
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'domain' => $this->normalizeDomain($domain),
|
||||||
|
'version_id' => null,
|
||||||
|
'status' => $status,
|
||||||
|
'activations_count' => $activationsCount,
|
||||||
|
'max_activations' => $maxActivations,
|
||||||
|
'expires_at' => $expiresAt ? $expiresAt->format('Y-m-d H:i:s') : null,
|
||||||
|
],
|
||||||
|
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all licenses to array format suitable for CSV
|
||||||
|
*
|
||||||
|
* @return array Array of license data for CSV export
|
||||||
|
*/
|
||||||
|
public function exportLicensesForCsv(): array
|
||||||
|
{
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$tableName = Installer::getLicensesTable();
|
||||||
|
$rows = $wpdb->get_results(
|
||||||
|
"SELECT * FROM {$tableName} ORDER BY created_at DESC",
|
||||||
|
ARRAY_A
|
||||||
|
);
|
||||||
|
|
||||||
|
$exportData = [];
|
||||||
|
foreach ($rows ?: [] as $row) {
|
||||||
|
$product = wc_get_product((int) $row['product_id']);
|
||||||
|
$customer = get_userdata((int) $row['customer_id']);
|
||||||
|
$order = wc_get_order((int) $row['order_id']);
|
||||||
|
|
||||||
|
$exportData[] = [
|
||||||
|
'ID' => $row['id'],
|
||||||
|
'License Key' => $row['license_key'],
|
||||||
|
'Product' => $product ? $product->get_name() : 'Unknown',
|
||||||
|
'Product ID' => $row['product_id'],
|
||||||
|
'Order ID' => $row['order_id'],
|
||||||
|
'Order Number' => $order ? $order->get_order_number() : '',
|
||||||
|
'Customer' => $customer ? $customer->display_name : 'Guest',
|
||||||
|
'Customer Email' => $customer ? $customer->user_email : '',
|
||||||
|
'Customer ID' => $row['customer_id'],
|
||||||
|
'Domain' => $row['domain'],
|
||||||
|
'Status' => ucfirst($row['status']),
|
||||||
|
'Activations' => $row['activations_count'],
|
||||||
|
'Max Activations' => $row['max_activations'],
|
||||||
|
'Expires At' => $row['expires_at'] ?: 'Lifetime',
|
||||||
|
'Created At' => $row['created_at'],
|
||||||
|
'Updated At' => $row['updated_at'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $exportData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||||||
namespace Jeremias\WcLicensedProduct;
|
namespace Jeremias\WcLicensedProduct;
|
||||||
|
|
||||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
||||||
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
||||||
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
|
use Jeremias\WcLicensedProduct\Checkout\CheckoutController;
|
||||||
@@ -117,6 +118,7 @@ final class Plugin
|
|||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
new AdminController($this->twig, $this->licenseManager);
|
new AdminController($this->twig, $this->licenseManager);
|
||||||
new VersionAdminController($this->versionManager);
|
new VersionAdminController($this->versionManager);
|
||||||
|
new SettingsController();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Jeremias\WcLicensedProduct\Product;
|
namespace Jeremias\WcLicensedProduct\Product;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
use WC_Product;
|
use WC_Product;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,28 +56,68 @@ class LicensedProduct extends WC_Product
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get max activations for this product
|
* Get max activations for this product
|
||||||
|
* Falls back to default settings if not set on product
|
||||||
*/
|
*/
|
||||||
public function get_max_activations(): int
|
public function get_max_activations(): int
|
||||||
{
|
{
|
||||||
$value = $this->get_meta('_licensed_max_activations', true);
|
$value = $this->get_meta('_licensed_max_activations', true);
|
||||||
return $value ? (int) $value : 1;
|
if ($value !== '' && $value !== null) {
|
||||||
|
return max(1, (int) $value);
|
||||||
|
}
|
||||||
|
return SettingsController::getDefaultMaxActivations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product has custom max activations set
|
||||||
|
*/
|
||||||
|
public function has_custom_max_activations(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_max_activations', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get validity days
|
* Get validity days
|
||||||
|
* Falls back to default settings if not set on product
|
||||||
*/
|
*/
|
||||||
public function get_validity_days(): ?int
|
public function get_validity_days(): ?int
|
||||||
{
|
{
|
||||||
$value = $this->get_meta('_licensed_validity_days', true);
|
$value = $this->get_meta('_licensed_validity_days', true);
|
||||||
return $value !== '' ? (int) $value : null;
|
if ($value !== '' && $value !== null) {
|
||||||
|
return (int) $value > 0 ? (int) $value : null;
|
||||||
|
}
|
||||||
|
return SettingsController::getDefaultValidityDays();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product has custom validity days set
|
||||||
|
*/
|
||||||
|
public function has_custom_validity_days(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_validity_days', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if license should be bound to major version
|
* Check if license should be bound to major version
|
||||||
|
* Falls back to default settings if not set on product
|
||||||
*/
|
*/
|
||||||
public function is_bound_to_version(): bool
|
public function is_bound_to_version(): bool
|
||||||
{
|
{
|
||||||
return $this->get_meta('_licensed_bind_to_version', true) === 'yes';
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
||||||
|
if ($value !== '' && $value !== null) {
|
||||||
|
return $value === 'yes';
|
||||||
|
}
|
||||||
|
return SettingsController::getDefaultBindToVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product has custom bind to version setting
|
||||||
|
*/
|
||||||
|
public function has_custom_bind_to_version(): bool
|
||||||
|
{
|
||||||
|
$value = $this->get_meta('_licensed_bind_to_version', true);
|
||||||
|
return $value !== '' && $value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Jeremias\WcLicensedProduct\Product;
|
namespace Jeremias\WcLicensedProduct\Product;
|
||||||
|
|
||||||
|
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers and handles the Licensed product type for WooCommerce
|
* Registers and handles the Licensed product type for WooCommerce
|
||||||
*/
|
*/
|
||||||
@@ -85,39 +87,82 @@ final class LicensedProductType
|
|||||||
public function addProductDataPanel(): void
|
public function addProductDataPanel(): void
|
||||||
{
|
{
|
||||||
global $post;
|
global $post;
|
||||||
|
|
||||||
|
// Get current product values
|
||||||
|
$currentMaxActivations = get_post_meta($post->ID, '_licensed_max_activations', true);
|
||||||
|
$currentValidityDays = get_post_meta($post->ID, '_licensed_validity_days', true);
|
||||||
|
$currentBindToVersion = get_post_meta($post->ID, '_licensed_bind_to_version', true);
|
||||||
|
|
||||||
|
// Get default values
|
||||||
|
$defaultMaxActivations = SettingsController::getDefaultMaxActivations();
|
||||||
|
$defaultValidityDays = SettingsController::getDefaultValidityDays();
|
||||||
|
$defaultBindToVersion = SettingsController::getDefaultBindToVersion();
|
||||||
|
|
||||||
|
// Format default validity for display
|
||||||
|
$defaultValidityDisplay = $defaultValidityDays !== null
|
||||||
|
? sprintf(__('%d days', 'wc-licensed-product'), $defaultValidityDays)
|
||||||
|
: __('Lifetime', 'wc-licensed-product');
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<div id="licensed_product_data" class="panel woocommerce_options_panel hidden">
|
<div id="licensed_product_data" class="panel woocommerce_options_panel hidden">
|
||||||
<div class="options_group">
|
<div class="options_group">
|
||||||
|
<p class="form-field">
|
||||||
|
<em><?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: URL to settings page */
|
||||||
|
esc_html__('Leave fields empty to use default settings from %s.', 'wc-licensed-product'),
|
||||||
|
'<a href="' . esc_url(admin_url('admin.php?page=wc-settings&tab=licensed_product')) . '">' .
|
||||||
|
esc_html__('WooCommerce > Settings > Licensed Products', 'wc-licensed-product') . '</a>'
|
||||||
|
);
|
||||||
|
?></em>
|
||||||
|
</p>
|
||||||
<?php
|
<?php
|
||||||
woocommerce_wp_text_input([
|
woocommerce_wp_text_input([
|
||||||
'id' => '_licensed_max_activations',
|
'id' => '_licensed_max_activations',
|
||||||
'label' => __('Max Activations', 'wc-licensed-product'),
|
'label' => __('Max Activations', 'wc-licensed-product'),
|
||||||
'description' => __('Maximum number of domain activations per license. Default: 1', 'wc-licensed-product'),
|
'description' => sprintf(
|
||||||
|
/* translators: %d: default max activations value */
|
||||||
|
__('Maximum number of domain activations per license. Default: %d', 'wc-licensed-product'),
|
||||||
|
$defaultMaxActivations
|
||||||
|
),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'custom_attributes' => [
|
'custom_attributes' => [
|
||||||
'min' => '1',
|
'min' => '1',
|
||||||
'step' => '1',
|
'step' => '1',
|
||||||
],
|
],
|
||||||
'value' => get_post_meta($post->ID, '_licensed_max_activations', true) ?: '1',
|
'placeholder' => (string) $defaultMaxActivations,
|
||||||
|
'value' => $currentMaxActivations,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
woocommerce_wp_text_input([
|
woocommerce_wp_text_input([
|
||||||
'id' => '_licensed_validity_days',
|
'id' => '_licensed_validity_days',
|
||||||
'label' => __('License Validity (Days)', 'wc-licensed-product'),
|
'label' => __('License Validity (Days)', 'wc-licensed-product'),
|
||||||
'description' => __('Number of days the license is valid. Leave empty for lifetime license.', 'wc-licensed-product'),
|
'description' => sprintf(
|
||||||
|
/* translators: %s: default validity value */
|
||||||
|
__('Number of days the license is valid. Leave empty for default (%s).', 'wc-licensed-product'),
|
||||||
|
$defaultValidityDisplay
|
||||||
|
),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'custom_attributes' => [
|
'custom_attributes' => [
|
||||||
'min' => '0',
|
'min' => '0',
|
||||||
'step' => '1',
|
'step' => '1',
|
||||||
],
|
],
|
||||||
|
'placeholder' => $defaultValidityDays !== null ? (string) $defaultValidityDays : __('Lifetime', 'wc-licensed-product'),
|
||||||
|
'value' => $currentValidityDays,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
woocommerce_wp_checkbox([
|
woocommerce_wp_checkbox([
|
||||||
'id' => '_licensed_bind_to_version',
|
'id' => '_licensed_bind_to_version',
|
||||||
'label' => __('Bind to Major Version', 'wc-licensed-product'),
|
'label' => __('Bind to Major Version', 'wc-licensed-product'),
|
||||||
'description' => __('If enabled, licenses are bound to the major version at purchase time.', 'wc-licensed-product'),
|
'description' => sprintf(
|
||||||
|
/* translators: %s: default bind to version value (Yes/No) */
|
||||||
|
__('If enabled, licenses are bound to the major version at purchase time. Default: %s', 'wc-licensed-product'),
|
||||||
|
$defaultBindToVersion ? __('Yes', 'wc-licensed-product') : __('No', 'wc-licensed-product')
|
||||||
|
),
|
||||||
|
'value' => $currentBindToVersion ?: ($defaultBindToVersion ? 'yes' : 'no'),
|
||||||
|
'cbvalue' => 'yes',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
woocommerce_wp_text_input([
|
woocommerce_wp_text_input([
|
||||||
@@ -160,19 +205,26 @@ final class LicensedProductType
|
|||||||
public function saveProductMeta(int $postId): void
|
public function saveProductMeta(int $postId): void
|
||||||
{
|
{
|
||||||
// Verify nonce is handled by WooCommerce
|
// Verify nonce is handled by WooCommerce
|
||||||
$maxActivations = isset($_POST['_licensed_max_activations'])
|
// Allow empty values to fall back to defaults
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified by WooCommerce
|
||||||
|
$maxActivations = isset($_POST['_licensed_max_activations']) && $_POST['_licensed_max_activations'] !== ''
|
||||||
? absint($_POST['_licensed_max_activations'])
|
? absint($_POST['_licensed_max_activations'])
|
||||||
: 1;
|
: '';
|
||||||
update_post_meta($postId, '_licensed_max_activations', $maxActivations);
|
update_post_meta($postId, '_licensed_max_activations', $maxActivations);
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
$validityDays = isset($_POST['_licensed_validity_days']) && $_POST['_licensed_validity_days'] !== ''
|
$validityDays = isset($_POST['_licensed_validity_days']) && $_POST['_licensed_validity_days'] !== ''
|
||||||
? absint($_POST['_licensed_validity_days'])
|
? absint($_POST['_licensed_validity_days'])
|
||||||
: '';
|
: '';
|
||||||
update_post_meta($postId, '_licensed_validity_days', $validityDays);
|
update_post_meta($postId, '_licensed_validity_days', $validityDays);
|
||||||
|
|
||||||
|
// For checkbox, we need to distinguish between "not set" and "explicitly unchecked"
|
||||||
|
// If the hidden field is present, the form was submitted and we save the actual value
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
$bindToVersion = isset($_POST['_licensed_bind_to_version']) ? 'yes' : 'no';
|
$bindToVersion = isset($_POST['_licensed_bind_to_version']) ? 'yes' : 'no';
|
||||||
update_post_meta($postId, '_licensed_bind_to_version', $bindToVersion);
|
update_post_meta($postId, '_licensed_bind_to_version', $bindToVersion);
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
$currentVersion = isset($_POST['_licensed_current_version'])
|
$currentVersion = isset($_POST['_licensed_current_version'])
|
||||||
? sanitize_text_field($_POST['_licensed_current_version'])
|
? sanitize_text_field($_POST['_licensed_current_version'])
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
176
templates/admin/dashboard.html.twig
Normal file
176
templates/admin/dashboard.html.twig
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<div class="wrap wclp-dashboard">
|
||||||
|
<h1>{{ __('License Dashboard') }}</h1>
|
||||||
|
|
||||||
|
<div class="wclp-dashboard-stats">
|
||||||
|
<!-- Overview Cards -->
|
||||||
|
<div class="wclp-stat-cards">
|
||||||
|
<div class="wclp-stat-card wclp-stat-total">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-admin-network"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.total }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Total Licenses') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-active">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-yes-alt"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.active }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Active') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-inactive">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-marker"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.inactive }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Inactive') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-expired">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-calendar-alt"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.expired }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Expired') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wclp-stat-card wclp-stat-revoked">
|
||||||
|
<div class="wclp-stat-icon"><span class="dashicons dashicons-dismiss"></span></div>
|
||||||
|
<div class="wclp-stat-content">
|
||||||
|
<span class="wclp-stat-number">{{ stats.by_status.revoked }}</span>
|
||||||
|
<span class="wclp-stat-label">{{ __('Revoked') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert: Expiring Soon -->
|
||||||
|
{% if stats.expiring_soon > 0 %}
|
||||||
|
<div class="notice notice-warning">
|
||||||
|
<p>
|
||||||
|
<span class="dashicons dashicons-warning"></span>
|
||||||
|
<strong>{{ __('Attention:') }}</strong>
|
||||||
|
{{ stats.expiring_soon }} {{ stats.expiring_soon == 1 ? __('license is') : __('licenses are') }}
|
||||||
|
{{ __('expiring within the next 30 days.') }}
|
||||||
|
<a href="{{ admin_url }}?page=wc-licenses">{{ __('View Licenses') }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Secondary Stats -->
|
||||||
|
<div class="wclp-stat-row">
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3>{{ __('License Types') }}</h3>
|
||||||
|
<table class="widefat striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{{ __('Lifetime Licenses') }}</td>
|
||||||
|
<td class="wclp-stat-value">{{ stats.lifetime }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ __('Time-limited Licenses') }}</td>
|
||||||
|
<td class="wclp-stat-value">{{ stats.expiring }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ __('Expiring Soon (30 days)') }}</td>
|
||||||
|
<td class="wclp-stat-value {% if stats.expiring_soon > 0 %}wclp-warning{% endif %}">
|
||||||
|
{{ stats.expiring_soon }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3>{{ __('Top Products by Licenses') }}</h3>
|
||||||
|
{% if stats.by_product is empty %}
|
||||||
|
<p class="description">{{ __('No license data available yet.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('Product') }}</th>
|
||||||
|
<th class="wclp-stat-value">{{ __('Licenses') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for product in stats.by_product %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ esc_html(product.product_name) }}</td>
|
||||||
|
<td class="wclp-stat-value">{{ product.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-stat-box">
|
||||||
|
<h3>{{ __('Top Domains') }}</h3>
|
||||||
|
{% if stats.top_domains is empty %}
|
||||||
|
<p class="description">{{ __('No license data available yet.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<table class="widefat striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ __('Domain') }}</th>
|
||||||
|
<th class="wclp-stat-value">{{ __('Licenses') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for domain in stats.top_domains %}
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ esc_html(domain.domain) }}</code></td>
|
||||||
|
<td class="wclp-stat-value">{{ domain.count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monthly Chart -->
|
||||||
|
<div class="wclp-stat-box wclp-stat-full">
|
||||||
|
<h3>{{ __('Licenses Created (Last 12 Months)') }}</h3>
|
||||||
|
{% if stats.monthly is empty %}
|
||||||
|
<p class="description">{{ __('No license data available yet.') }}</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="wclp-chart-container">
|
||||||
|
<div class="wclp-bar-chart" id="wclp-monthly-chart">
|
||||||
|
{% set max_value = 1 %}
|
||||||
|
{% for count in stats.monthly %}
|
||||||
|
{% if count > max_value %}
|
||||||
|
{% set max_value = count %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for month, count in stats.monthly %}
|
||||||
|
<div class="wclp-bar-wrapper">
|
||||||
|
<div class="wclp-bar" style="height: {{ (count / max_value * 100)|round }}%;" title="{{ count }} {{ __('licenses') }}">
|
||||||
|
<span class="wclp-bar-value">{{ count }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="wclp-bar-label">{{ month|date('M Y') }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="wclp-dashboard-actions">
|
||||||
|
<h2>{{ __('Quick Actions') }}</h2>
|
||||||
|
<div class="wclp-action-buttons">
|
||||||
|
<a href="{{ admin_url }}?page=wc-licenses" class="button button-primary">
|
||||||
|
<span class="dashicons dashicons-admin-network"></span>
|
||||||
|
{{ __('Manage Licenses') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ admin_url }}?page=wc-licenses&action=export_csv" class="button">
|
||||||
|
<span class="dashicons dashicons-download"></span>
|
||||||
|
{{ __('Export to CSV') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ admin_url }}?page=wc-settings&tab=licensed_product" class="button">
|
||||||
|
<span class="dashicons dashicons-admin-generic"></span>
|
||||||
|
{{ __('Settings') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>{{ __('Licenses') }}</h1>
|
<h1 class="wp-heading-inline">{{ __('Licenses') }}</h1>
|
||||||
|
<a href="{{ admin_url }}?action=export_csv" class="page-title-action">
|
||||||
|
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
|
||||||
|
{{ __('Export CSV') }}
|
||||||
|
</a>
|
||||||
|
<a href="{{ admin_url }}?action=import_csv" class="page-title-action">
|
||||||
|
<span class="dashicons dashicons-upload" style="vertical-align: middle;"></span>
|
||||||
|
{{ __('Import CSV') }}
|
||||||
|
</a>
|
||||||
|
<hr class="wp-header-end">
|
||||||
|
|
||||||
{% for notice in notices %}
|
{% for notice in notices %}
|
||||||
<div class="notice notice-{{ notice.type }} is-dismissible">
|
<div class="notice notice-{{ notice.type }} is-dismissible">
|
||||||
@@ -7,13 +16,80 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<p class="description">
|
<!-- Search and Filter Form -->
|
||||||
{{ __('Total licenses:') }} <strong>{{ total_licenses }}</strong>
|
<form method="get" action="" class="wclp-filter-form">
|
||||||
|
<input type="hidden" name="page" value="wc-licenses">
|
||||||
|
|
||||||
|
<p class="search-box">
|
||||||
|
<label class="screen-reader-text" for="license-search-input">{{ __('Search Licenses') }}</label>
|
||||||
|
<input type="search" id="license-search-input" name="s" value="{{ filters.search|default('') }}"
|
||||||
|
placeholder="{{ __('Search license key or domain...') }}">
|
||||||
|
<input type="submit" id="search-submit" class="button" value="{{ __('Search') }}">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="wp-list-table widefat fixed striped">
|
<div class="tablenav top">
|
||||||
|
<div class="alignleft actions">
|
||||||
|
<select name="status">
|
||||||
|
<option value="all">{{ __('All Statuses') }}</option>
|
||||||
|
<option value="active" {{ filters.status|default('') == 'active' ? 'selected' : '' }}>{{ __('Active') }}</option>
|
||||||
|
<option value="inactive" {{ filters.status|default('') == 'inactive' ? 'selected' : '' }}>{{ __('Inactive') }}</option>
|
||||||
|
<option value="expired" {{ filters.status|default('') == 'expired' ? 'selected' : '' }}>{{ __('Expired') }}</option>
|
||||||
|
<option value="revoked" {{ filters.status|default('') == 'revoked' ? 'selected' : '' }}>{{ __('Revoked') }}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select name="product_id">
|
||||||
|
<option value="">{{ __('All Products') }}</option>
|
||||||
|
{% for id, name in products %}
|
||||||
|
<option value="{{ id }}" {{ filters.product_id|default('') == id ? 'selected' : '' }}>{{ esc_html(name) }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type="submit" class="button" value="{{ __('Filter') }}">
|
||||||
|
|
||||||
|
{% if filters is not empty %}
|
||||||
|
<a href="{{ admin_url }}" class="button">{{ __('Clear') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tablenav-pages">
|
||||||
|
<span class="displaying-num">{{ total_licenses }} {{ total_licenses == 1 ? __('item') : __('items') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="description">
|
||||||
|
{{ __('Showing') }} {{ total_licenses }} {{ total_licenses == 1 ? __('license') : __('licenses') }}
|
||||||
|
{% if filters is not empty %}
|
||||||
|
({{ __('filtered') }})
|
||||||
|
{% endif %}
|
||||||
|
| <a href="{{ constant('admin_url')('admin.php?page=wc-reports&tab=licenses') }}">{{ __('View Dashboard') }}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" action="{{ admin_url }}" id="licenses-bulk-form">
|
||||||
|
{{ wp_nonce_field('bulk_license_action', '_wpnonce', true, false)|raw }}
|
||||||
|
|
||||||
|
<div class="tablenav top">
|
||||||
|
<div class="alignleft actions bulkactions">
|
||||||
|
<select name="bulk_action" id="bulk-action-selector">
|
||||||
|
<option value="">{{ __('Bulk Actions') }}</option>
|
||||||
|
<option value="activate">{{ __('Activate') }}</option>
|
||||||
|
<option value="deactivate">{{ __('Deactivate') }}</option>
|
||||||
|
<option value="revoke">{{ __('Revoke') }}</option>
|
||||||
|
<option value="extend_30">{{ __('Extend 30 days') }}</option>
|
||||||
|
<option value="extend_90">{{ __('Extend 90 days') }}</option>
|
||||||
|
<option value="extend_365">{{ __('Extend 1 year') }}</option>
|
||||||
|
<option value="delete">{{ __('Delete') }}</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" class="button action" value="{{ __('Apply') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="wp-list-table widefat fixed striped licenses-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="manage-column column-cb check-column">
|
||||||
|
<input type="checkbox" id="cb-select-all-1">
|
||||||
|
</td>
|
||||||
<th>{{ __('License Key') }}</th>
|
<th>{{ __('License Key') }}</th>
|
||||||
<th>{{ __('Product') }}</th>
|
<th>{{ __('Product') }}</th>
|
||||||
<th>{{ __('Customer') }}</th>
|
<th>{{ __('Customer') }}</th>
|
||||||
@@ -26,11 +102,14 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% if licenses is empty %}
|
{% if licenses is empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7">{{ __('No licenses found.') }}</td>
|
<td colspan="8">{{ __('No licenses found.') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for item in licenses %}
|
{% for item in licenses %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<th scope="row" class="check-column">
|
||||||
|
<input type="checkbox" name="license_ids[]" value="{{ item.license.id }}">
|
||||||
|
</th>
|
||||||
<td><code>{{ item.license.licenseKey }}</code></td>
|
<td><code>{{ item.license.licenseKey }}</code></td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.product_edit_url %}
|
{% if item.product_edit_url %}
|
||||||
@@ -55,35 +134,80 @@
|
|||||||
{% if item.license.expiresAt %}
|
{% if item.license.expiresAt %}
|
||||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
{{ item.license.expiresAt|date('Y-m-d') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ __('Never') }}
|
<span class="license-lifetime">{{ __('Lifetime') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="license-actions">
|
||||||
|
<div class="row-actions">
|
||||||
{% if item.license.status != 'revoked' %}
|
{% if item.license.status != 'revoked' %}
|
||||||
<a href="{{ admin_url ~ '?page=wc-licenses&action=revoke&license_id=' ~ item.license.id ~ '&_wpnonce=' }}"
|
<span class="transfer">
|
||||||
class="button button-small"
|
<a href="#" class="wclp-transfer-link"
|
||||||
onclick="return confirm('{{ __('Are you sure?') }}')">
|
data-license-id="{{ item.license.id }}"
|
||||||
{{ __('Revoke') }}
|
data-current-domain="{{ esc_attr(item.license.domain) }}"
|
||||||
</a>
|
title="{{ __('Transfer to new domain') }}">{{ __('Transfer') }}</a> |
|
||||||
|
</span>
|
||||||
|
<span class="extend">
|
||||||
|
<a href="{{ extend_url(item.license.id, 30) }}" title="{{ __('Extend by 30 days') }}">+30d</a> |
|
||||||
|
</span>
|
||||||
|
<span class="lifetime">
|
||||||
|
<a href="{{ lifetime_url(item.license.id) }}" title="{{ __('Set to lifetime') }}">∞</a> |
|
||||||
|
</span>
|
||||||
|
<span class="revoke">
|
||||||
|
<a href="{{ revoke_url(item.license.id) }}"
|
||||||
|
onclick="return confirm('{{ __('Are you sure?') }}')">{{ __('Revoke') }}</a> |
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ admin_url ~ '?page=wc-licenses&action=delete&license_id=' ~ item.license.id ~ '&_wpnonce=' }}"
|
<span class="delete">
|
||||||
class="button button-small button-link-delete"
|
<a href="{{ delete_url(item.license.id) }}"
|
||||||
onclick="return confirm('{{ __('Are you sure you want to delete this license?') }}')">
|
class="submitdelete"
|
||||||
{{ __('Delete') }}
|
onclick="return confirm('{{ __('Are you sure you want to delete this license?') }}')">{{ __('Delete') }}</a>
|
||||||
</a>
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td class="manage-column column-cb check-column">
|
||||||
|
<input type="checkbox" id="cb-select-all-2">
|
||||||
|
</td>
|
||||||
|
<th>{{ __('License Key') }}</th>
|
||||||
|
<th>{{ __('Product') }}</th>
|
||||||
|
<th>{{ __('Customer') }}</th>
|
||||||
|
<th>{{ __('Domain') }}</th>
|
||||||
|
<th>{{ __('Status') }}</th>
|
||||||
|
<th>{{ __('Expires') }}</th>
|
||||||
|
<th>{{ __('Actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% if total_pages > 1 %}
|
|
||||||
<div class="tablenav bottom">
|
<div class="tablenav bottom">
|
||||||
|
<div class="alignleft actions bulkactions">
|
||||||
|
<select name="bulk_action_2" id="bulk-action-selector-bottom">
|
||||||
|
<option value="">{{ __('Bulk Actions') }}</option>
|
||||||
|
<option value="activate">{{ __('Activate') }}</option>
|
||||||
|
<option value="deactivate">{{ __('Deactivate') }}</option>
|
||||||
|
<option value="revoke">{{ __('Revoke') }}</option>
|
||||||
|
<option value="extend_30">{{ __('Extend 30 days') }}</option>
|
||||||
|
<option value="extend_90">{{ __('Extend 90 days') }}</option>
|
||||||
|
<option value="extend_365">{{ __('Extend 1 year') }}</option>
|
||||||
|
<option value="delete">{{ __('Delete') }}</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" class="button action" value="{{ __('Apply') }}">
|
||||||
|
</div>
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
{% set filter_params = '' %}
|
||||||
|
{% if filters.search is defined and filters.search %}{% set filter_params = filter_params ~ '&s=' ~ filters.search %}{% endif %}
|
||||||
|
{% if filters.status is defined and filters.status %}{% set filter_params = filter_params ~ '&status=' ~ filters.status %}{% endif %}
|
||||||
|
{% if filters.product_id is defined and filters.product_id %}{% set filter_params = filter_params ~ '&product_id=' ~ filters.product_id %}{% endif %}
|
||||||
|
|
||||||
<div class="tablenav-pages">
|
<div class="tablenav-pages">
|
||||||
<span class="pagination-links">
|
<span class="pagination-links">
|
||||||
{% if current_page > 1 %}
|
{% if current_page > 1 %}
|
||||||
<a class="prev-page button" href="{{ admin_url ~ '&paged=' ~ (current_page - 1) }}">
|
<a class="prev-page button" href="{{ admin_url ~ '&paged=' ~ (current_page - 1) ~ filter_params }}">
|
||||||
<span aria-hidden="true">‹</span>
|
<span aria-hidden="true">‹</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -91,12 +215,93 @@
|
|||||||
{{ current_page }} {{ __('of') }} {{ total_pages }}
|
{{ current_page }} {{ __('of') }} {{ total_pages }}
|
||||||
</span>
|
</span>
|
||||||
{% if current_page < total_pages %}
|
{% if current_page < total_pages %}
|
||||||
<a class="next-page button" href="{{ admin_url ~ '&paged=' ~ (current_page + 1) }}">
|
<a class="next-page button" href="{{ admin_url ~ '&paged=' ~ (current_page + 1) ~ filter_params }}">
|
||||||
<span aria-hidden="true">›</span>
|
<span aria-hidden="true">›</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
</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>{{ __('Transfer License to New Domain') }}</h2>
|
||||||
|
<form method="post" action="{{ admin_url }}">
|
||||||
|
<input type="hidden" name="action" value="transfer_license">
|
||||||
|
<input type="hidden" name="_wpnonce" value="{{ transfer_nonce() }}">
|
||||||
|
<input type="hidden" name="license_id" id="transfer-license-id" value="">
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label>{{ __('Current Domain') }}</label></th>
|
||||||
|
<td><code id="transfer-current-domain"></code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="new_domain">{{ __('New Domain') }}</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="new_domain" id="transfer-new-domain" class="regular-text"
|
||||||
|
placeholder="example.com" required>
|
||||||
|
<p class="description">{{ __('Enter the new domain without http:// or www.') }}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p class="submit">
|
||||||
|
<button type="submit" class="button button-primary">{{ __('Transfer License') }}</button>
|
||||||
|
<button type="button" class="button wclp-modal-cancel">{{ __('Cancel') }}</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function($) {
|
||||||
|
// Select all checkboxes
|
||||||
|
$('#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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync bulk action selects
|
||||||
|
$('#bulk-action-selector, #bulk-action-selector-bottom').on('change', function() {
|
||||||
|
var value = $(this).val();
|
||||||
|
$('#bulk-action-selector, #bulk-action-selector-bottom').val(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use bottom select value if top is empty
|
||||||
|
$('form').on('submit', function() {
|
||||||
|
var topAction = $('#bulk-action-selector').val();
|
||||||
|
var bottomAction = $('#bulk-action-selector-bottom').val();
|
||||||
|
if (!topAction && bottomAction) {
|
||||||
|
$('#bulk-action-selector').val(bottomAction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transfer modal
|
||||||
|
var $modal = $('#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('');
|
||||||
|
$modal.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
|
||||||
|
$modal.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).on('click', function(e) {
|
||||||
|
if ($(e.target).is($modal)) {
|
||||||
|
$modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -29,7 +29,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="license-info-row">
|
<div class="license-info-row">
|
||||||
<span><strong>{{ __('Domain:') }}</strong> {{ esc_html(item.license.domain) }}</span>
|
<span class="license-domain-display" data-license-id="{{ item.license.id }}">
|
||||||
|
<strong>{{ __('Domain:') }}</strong>
|
||||||
|
<span class="domain-value">{{ esc_html(item.license.domain) }}</span>
|
||||||
|
{% if item.license.status == 'active' or item.license.status == 'inactive' %}
|
||||||
|
<button type="button" class="wclp-transfer-btn"
|
||||||
|
data-license-id="{{ item.license.id }}"
|
||||||
|
data-current-domain="{{ esc_attr(item.license.domain) }}"
|
||||||
|
title="{{ __('Transfer to new domain') }}">
|
||||||
|
<span class="dashicons dashicons-randomize"></span>
|
||||||
|
{{ __('Transfer') }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
<span><strong>{{ __('Expires:') }}</strong>
|
<span><strong>{{ __('Expires:') }}</strong>
|
||||||
{% if item.license.expiresAt %}
|
{% if item.license.expiresAt %}
|
||||||
{{ item.license.expiresAt|date('Y-m-d') }}
|
{{ item.license.expiresAt|date('Y-m-d') }}
|
||||||
@@ -60,4 +72,38 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Transfer Modal -->
|
||||||
|
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
|
||||||
|
<div class="wclp-modal-overlay"></div>
|
||||||
|
<div class="wclp-modal-content">
|
||||||
|
<button type="button" class="wclp-modal-close" aria-label="{{ __('Close') }}">×</button>
|
||||||
|
<h3>{{ __('Transfer License to New Domain') }}</h3>
|
||||||
|
<form id="wclp-transfer-form">
|
||||||
|
<input type="hidden" name="license_id" id="transfer-license-id" value="">
|
||||||
|
|
||||||
|
<div class="wclp-form-row">
|
||||||
|
<label>{{ __('Current Domain') }}</label>
|
||||||
|
<p class="wclp-current-domain"><code id="transfer-current-domain"></code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-form-row">
|
||||||
|
<label for="transfer-new-domain">{{ __('New Domain') }}</label>
|
||||||
|
<input type="text" name="new_domain" id="transfer-new-domain"
|
||||||
|
placeholder="example.com" required
|
||||||
|
pattern="[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+">
|
||||||
|
<p class="wclp-field-description">{{ __('Enter the new domain without http:// or www.') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wclp-form-row wclp-form-actions">
|
||||||
|
<button type="submit" class="button wclp-btn-primary" id="wclp-transfer-submit">
|
||||||
|
{{ __('Transfer License') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="button wclp-modal-cancel">{{ __('Cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wclp-transfer-message" class="wclp-message" style="display:none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -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.0.3
|
* Version: 0.0.7
|
||||||
* 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.0.3');
|
define('WC_LICENSED_PRODUCT_VERSION', '0.0.7');
|
||||||
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