You've already forked wc-licensed-product
Implement version 0.0.10 features
- Add license meta box on WooCommerce order edit pages - Add editable order domain field with AJAX inline editing - Add editable license domains directly from order page - Add licenses table showing all licenses for an order - Support both classic orders and HPOS New files: - src/Admin/OrderLicenseController.php - assets/js/order-licenses.js New method: LicenseManager::getLicensesByOrder() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
57
CHANGELOG.md
57
CHANGELOG.md
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.10] - 2026-01-21
|
||||
|
||||
### Added
|
||||
|
||||
- License meta box on WooCommerce order edit pages
|
||||
- Editable order domain field with AJAX save
|
||||
- Editable license domains directly from order page
|
||||
- View licenses table showing all licenses for an order
|
||||
- Link to full licenses management page from order view
|
||||
- Support for both classic orders and HPOS (High-Performance Order Storage)
|
||||
|
||||
### Technical Details
|
||||
|
||||
- New `OrderLicenseController` class for order page integration
|
||||
- New `getLicensesByOrder()` method in LicenseManager
|
||||
- JavaScript file `order-licenses.js` for inline domain editing
|
||||
- AJAX handlers for updating order domain and license domains
|
||||
|
||||
## [0.0.9] - 2026-01-21
|
||||
|
||||
### Added
|
||||
|
||||
- API client examples for multiple programming languages
|
||||
- cURL command examples (`docs/client-examples/curl.sh`)
|
||||
- PHP client class (`docs/client-examples/php-client.php`)
|
||||
- Python client class (`docs/client-examples/python-client.py`)
|
||||
- JavaScript/Node.js client (`docs/client-examples/javascript-client.js`)
|
||||
- C# client class (`docs/client-examples/csharp-client.cs`)
|
||||
- Client examples documentation (`docs/client-examples/README.md`)
|
||||
|
||||
### Technical Details
|
||||
|
||||
- All clients include rate limiting handling (HTTP 429)
|
||||
- Examples demonstrate validate, status, and activate endpoints
|
||||
- JavaScript client works in both browser and Node.js environments
|
||||
- Python client uses dataclasses for type-safe responses
|
||||
- C# client uses async/await patterns
|
||||
|
||||
## [0.0.8] - 2026-01-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Current version now automatically derived from latest product version
|
||||
- Email system refactored to use WooCommerce transactional emails
|
||||
- License expiration warning email now configurable via WooCommerce email settings
|
||||
|
||||
### Removed
|
||||
|
||||
- "Current Version" field from product license settings panel
|
||||
|
||||
### Technical Details
|
||||
|
||||
- New `LicenseExpirationEmail` class extends WC_Email
|
||||
- LicensedProduct's `get_current_version()` queries VersionManager
|
||||
- Uses WooCommerce email header/footer templates
|
||||
- Warning days configurable in plugin settings
|
||||
|
||||
## [0.0.7] - 2026-01-21
|
||||
|
||||
### Added
|
||||
|
||||
32
CLAUDE.md
32
CLAUDE.md
@@ -36,10 +36,9 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
||||
|
||||
None currently known.
|
||||
|
||||
### Version 0.0.10
|
||||
### Version 0.0.11 (planned)
|
||||
|
||||
- Add a license related form section to the orders form in the admin area
|
||||
- Make license domains editable in the backend
|
||||
- TBD - no specific features planned yet
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -527,3 +526,30 @@ Full API documentation available in `openapi.json` (OpenAPI 3.1 specification).
|
||||
- `$wpdb->insert()` now only includes `attachment_id` field when it has a valid value
|
||||
- Added error logging for version creation failures to aid debugging
|
||||
- Improved meta box visibility logic for new products
|
||||
|
||||
### 2026-01-21 - Version 0.0.10 Features
|
||||
|
||||
**Implemented:**
|
||||
|
||||
- License meta box on WooCommerce order edit pages
|
||||
- Editable order domain field with AJAX-based inline editing
|
||||
- Editable license domains directly from the order page
|
||||
- Licenses table showing all licenses associated with an order
|
||||
- Support for both classic orders and HPOS (High-Performance Order Storage)
|
||||
|
||||
**New files:**
|
||||
|
||||
- `src/Admin/OrderLicenseController.php` - Order page license integration
|
||||
- `assets/js/order-licenses.js` - JavaScript for inline domain editing
|
||||
|
||||
**New methods in LicenseManager:**
|
||||
|
||||
- `getLicensesByOrder()` - Get all licenses for a specific order
|
||||
|
||||
**Technical notes:**
|
||||
|
||||
- Meta box automatically detects HPOS vs classic order storage
|
||||
- AJAX handlers for updating both order domain and individual license domains
|
||||
- Domain validation with normalization (strips protocol, www prefix)
|
||||
- Inline edit UI with save/cancel for license domains
|
||||
- Links to full licenses management page for advanced actions
|
||||
|
||||
206
assets/js/order-licenses.js
Normal file
206
assets/js/order-licenses.js
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* WC Licensed Product - Order Licenses Admin
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
var WCLPOrderLicenses = {
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
// Order domain save
|
||||
$('#wclp-save-order-domain').on('click', this.saveOrderDomain.bind(this));
|
||||
|
||||
// License domain edit/save/cancel
|
||||
$(document).on('click', '.wclp-edit-domain-btn', this.startEditDomain);
|
||||
$(document).on('click', '.wclp-save-domain-btn', this.saveLicenseDomain.bind(this));
|
||||
$(document).on('click', '.wclp-cancel-domain-btn', this.cancelEditDomain);
|
||||
|
||||
// Enter key on domain inputs
|
||||
$('#wclp-order-domain').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
e.preventDefault();
|
||||
$('#wclp-save-order-domain').click();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('keypress', '.wclp-domain-input', function(e) {
|
||||
if (e.which === 13) {
|
||||
e.preventDefault();
|
||||
$(this).closest('.wclp-license-domain-edit').find('.wclp-save-domain-btn').click();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Save order domain
|
||||
*/
|
||||
saveOrderDomain: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $('#wclp-save-order-domain');
|
||||
var $input = $('#wclp-order-domain');
|
||||
var $spinner = $btn.siblings('.spinner');
|
||||
var $message = $btn.siblings('.wclp-status-message');
|
||||
|
||||
var orderId = $input.data('order-id');
|
||||
var domain = $input.val().trim();
|
||||
|
||||
$btn.prop('disabled', true);
|
||||
$spinner.addClass('is-active');
|
||||
$message.text('').removeClass('success error');
|
||||
|
||||
$.ajax({
|
||||
url: wclpOrderLicenses.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wclp_update_order_domain',
|
||||
nonce: wclpOrderLicenses.nonce,
|
||||
order_id: orderId,
|
||||
domain: domain
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$input.val(response.data.domain);
|
||||
$message.text(wclpOrderLicenses.strings.saved).addClass('success');
|
||||
} else {
|
||||
$message.text(response.data.message || wclpOrderLicenses.strings.error).addClass('error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$message.text(wclpOrderLicenses.strings.error).addClass('error');
|
||||
},
|
||||
complete: function() {
|
||||
$btn.prop('disabled', false);
|
||||
$spinner.removeClass('is-active');
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(function() {
|
||||
$message.text('').removeClass('success error');
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Start editing license domain
|
||||
*/
|
||||
startEditDomain: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $container = $(this).closest('.wclp-license-domain-edit');
|
||||
var $display = $container.find('.wclp-domain-display');
|
||||
var $input = $container.find('.wclp-domain-input');
|
||||
var $editBtn = $container.find('.wclp-edit-domain-btn');
|
||||
var $saveBtn = $container.find('.wclp-save-domain-btn');
|
||||
var $cancelBtn = $container.find('.wclp-cancel-domain-btn');
|
||||
|
||||
// Store original value
|
||||
$input.data('original', $display.text());
|
||||
|
||||
// Toggle visibility
|
||||
$display.hide();
|
||||
$editBtn.hide();
|
||||
$input.show().focus().select();
|
||||
$saveBtn.show();
|
||||
$cancelBtn.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel editing license domain
|
||||
*/
|
||||
cancelEditDomain: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $container = $(this).closest('.wclp-license-domain-edit');
|
||||
var $display = $container.find('.wclp-domain-display');
|
||||
var $input = $container.find('.wclp-domain-input');
|
||||
var $editBtn = $container.find('.wclp-edit-domain-btn');
|
||||
var $saveBtn = $container.find('.wclp-save-domain-btn');
|
||||
var $cancelBtn = $container.find('.wclp-cancel-domain-btn');
|
||||
|
||||
// Restore original value
|
||||
$input.val($input.data('original'));
|
||||
|
||||
// Toggle visibility
|
||||
$input.hide();
|
||||
$saveBtn.hide();
|
||||
$cancelBtn.hide();
|
||||
$display.show();
|
||||
$editBtn.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Save license domain
|
||||
*/
|
||||
saveLicenseDomain: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $btn = $(e.currentTarget);
|
||||
var $container = $btn.closest('.wclp-license-domain-edit');
|
||||
var $row = $btn.closest('tr');
|
||||
var $input = $container.find('.wclp-domain-input');
|
||||
var $display = $container.find('.wclp-domain-display');
|
||||
var $spinner = $container.find('.spinner');
|
||||
var $editBtn = $container.find('.wclp-edit-domain-btn');
|
||||
var $cancelBtn = $container.find('.wclp-cancel-domain-btn');
|
||||
|
||||
var licenseId = $row.data('license-id');
|
||||
var domain = $input.val().trim();
|
||||
|
||||
if (!domain) {
|
||||
alert(wclpOrderLicenses.strings.invalidDomain);
|
||||
return;
|
||||
}
|
||||
|
||||
$btn.prop('disabled', true);
|
||||
$cancelBtn.prop('disabled', true);
|
||||
$spinner.addClass('is-active');
|
||||
|
||||
$.ajax({
|
||||
url: wclpOrderLicenses.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wclp_update_license_domain',
|
||||
nonce: wclpOrderLicenses.nonce,
|
||||
license_id: licenseId,
|
||||
domain: domain
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Update display
|
||||
$display.text(response.data.domain);
|
||||
$input.val(response.data.domain);
|
||||
|
||||
// Switch back to display mode
|
||||
$input.hide();
|
||||
$btn.hide();
|
||||
$cancelBtn.hide();
|
||||
$display.show();
|
||||
$editBtn.show();
|
||||
} else {
|
||||
alert(response.data.message || wclpOrderLicenses.strings.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert(wclpOrderLicenses.strings.error);
|
||||
},
|
||||
complete: function() {
|
||||
$btn.prop('disabled', false);
|
||||
$cancelBtn.prop('disabled', false);
|
||||
$spinner.removeClass('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
WCLPOrderLicenses.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
395
src/Admin/OrderLicenseController.php
Normal file
395
src/Admin/OrderLicenseController.php
Normal file
@@ -0,0 +1,395 @@
|
||||
<?php
|
||||
/**
|
||||
* Order License Admin Controller
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Admin
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Admin;
|
||||
|
||||
use Jeremias\WcLicensedProduct\License\License;
|
||||
use Jeremias\WcLicensedProduct\License\LicenseManager;
|
||||
|
||||
/**
|
||||
* Handles license display and editing on order admin pages
|
||||
*/
|
||||
final class OrderLicenseController
|
||||
{
|
||||
private LicenseManager $licenseManager;
|
||||
|
||||
public function __construct(LicenseManager $licenseManager)
|
||||
{
|
||||
$this->licenseManager = $licenseManager;
|
||||
$this->registerHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks
|
||||
*/
|
||||
private function registerHooks(): void
|
||||
{
|
||||
// Add licenses meta box to order edit page
|
||||
add_action('add_meta_boxes', [$this, 'addLicensesMetaBox']);
|
||||
|
||||
// Handle AJAX actions
|
||||
add_action('wp_ajax_wclp_update_order_domain', [$this, 'ajaxUpdateOrderDomain']);
|
||||
add_action('wp_ajax_wclp_update_license_domain', [$this, 'ajaxUpdateLicenseDomain']);
|
||||
|
||||
// Enqueue admin scripts
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueScripts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add licenses meta box to order edit page
|
||||
*/
|
||||
public function addLicensesMetaBox(): void
|
||||
{
|
||||
// Support both classic post type and HPOS
|
||||
$screen = wc_get_container()->get(\Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class)->custom_orders_table_usage_is_enabled()
|
||||
? wc_get_page_screen_id('shop-order')
|
||||
: 'shop_order';
|
||||
|
||||
add_meta_box(
|
||||
'wclp_order_licenses',
|
||||
__('Product Licenses', 'wc-licensed-product'),
|
||||
[$this, 'renderLicensesMetaBox'],
|
||||
$screen,
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render licenses meta box
|
||||
*/
|
||||
public function renderLicensesMetaBox($post_or_order): void
|
||||
{
|
||||
// Get order object - support both classic and HPOS
|
||||
if ($post_or_order instanceof \WC_Order) {
|
||||
$order = $post_or_order;
|
||||
} else {
|
||||
$order = wc_get_order($post_or_order->ID);
|
||||
}
|
||||
|
||||
if (!$order) {
|
||||
echo '<p>' . esc_html__('Order not found.', 'wc-licensed-product') . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if order has licensed products
|
||||
$hasLicensedProduct = false;
|
||||
foreach ($order->get_items() as $item) {
|
||||
$product = $item->get_product();
|
||||
if ($product && $product->is_type('licensed')) {
|
||||
$hasLicensedProduct = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasLicensedProduct) {
|
||||
echo '<p>' . esc_html__('This order does not contain licensed products.', 'wc-licensed-product') . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get order domain
|
||||
$orderDomain = $order->get_meta('_licensed_product_domain');
|
||||
|
||||
// Get licenses for this order
|
||||
$licenses = $this->licenseManager->getLicensesByOrder($order->get_id());
|
||||
|
||||
wp_nonce_field('wclp_order_license_actions', 'wclp_order_license_nonce');
|
||||
?>
|
||||
<div class="wclp-order-licenses">
|
||||
<div class="wclp-order-domain-section">
|
||||
<h4><?php esc_html_e('Order Domain', 'wc-licensed-product'); ?></h4>
|
||||
<p class="description">
|
||||
<?php esc_html_e('The domain specified during checkout. Changing this will not automatically update existing license domains.', 'wc-licensed-product'); ?>
|
||||
</p>
|
||||
<div class="wclp-inline-edit">
|
||||
<input type="text"
|
||||
id="wclp-order-domain"
|
||||
class="regular-text"
|
||||
value="<?php echo esc_attr($orderDomain); ?>"
|
||||
data-order-id="<?php echo esc_attr($order->get_id()); ?>"
|
||||
placeholder="<?php esc_attr_e('example.com', 'wc-licensed-product'); ?>" />
|
||||
<button type="button" class="button" id="wclp-save-order-domain">
|
||||
<?php esc_html_e('Save', 'wc-licensed-product'); ?>
|
||||
</button>
|
||||
<span class="spinner"></span>
|
||||
<span class="wclp-status-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h4>
|
||||
|
||||
<?php if (empty($licenses)): ?>
|
||||
<p class="description">
|
||||
<?php esc_html_e('No licenses have been generated for this order yet.', 'wc-licensed-product'); ?>
|
||||
<?php if ($order->is_paid()): ?>
|
||||
<br />
|
||||
<em><?php esc_html_e('Licenses should be generated automatically when an order is paid. If missing, check that a domain was specified during checkout.', 'wc-licensed-product'); ?></em>
|
||||
<?php else: ?>
|
||||
<br />
|
||||
<em><?php esc_html_e('Licenses will be generated when the order is marked as paid/completed.', 'wc-licensed-product'); ?></em>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<table class="widefat striped wclp-licenses-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Product', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
|
||||
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($licenses as $license): ?>
|
||||
<?php
|
||||
$product = wc_get_product($license->getProductId());
|
||||
$statusClass = 'wclp-status-' . $license->getStatus();
|
||||
?>
|
||||
<tr data-license-id="<?php echo esc_attr($license->getId()); ?>">
|
||||
<td>
|
||||
<code class="wclp-license-key"><?php echo esc_html($license->getLicenseKey()); ?></code>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($product): ?>
|
||||
<a href="<?php echo esc_url(get_edit_post_link($product->get_id())); ?>">
|
||||
<?php echo esc_html($product->get_name()); ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?php esc_html_e('Unknown', 'wc-licensed-product'); ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="wclp-license-domain-edit">
|
||||
<span class="wclp-domain-display"><?php echo esc_html($license->getDomain()); ?></span>
|
||||
<input type="text"
|
||||
class="wclp-domain-input regular-text"
|
||||
value="<?php echo esc_attr($license->getDomain()); ?>"
|
||||
style="display: none;" />
|
||||
<button type="button" class="button-link wclp-edit-domain-btn" title="<?php esc_attr_e('Edit domain', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-edit"></span>
|
||||
</button>
|
||||
<button type="button" class="button button-small wclp-save-domain-btn" style="display: none;">
|
||||
<?php esc_html_e('Save', 'wc-licensed-product'); ?>
|
||||
</button>
|
||||
<button type="button" class="button-link wclp-cancel-domain-btn" style="display: none;">
|
||||
<?php esc_html_e('Cancel', 'wc-licensed-product'); ?>
|
||||
</button>
|
||||
<span class="spinner"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="wclp-license-status <?php echo esc_attr($statusClass); ?>">
|
||||
<?php echo esc_html(ucfirst($license->getStatus())); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$expiresAt = $license->getExpiresAt();
|
||||
if ($expiresAt) {
|
||||
echo esc_html($expiresAt->format(get_option('date_format')));
|
||||
} else {
|
||||
echo '<span class="wclp-lifetime">' . esc_html__('Lifetime', 'wc-licensed-product') . '</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&s=' . urlencode($license->getLicenseKey()))); ?>"
|
||||
class="button button-small"
|
||||
title="<?php esc_attr_e('View in Licenses', 'wc-licensed-product'); ?>">
|
||||
<span class="dashicons dashicons-visibility" style="vertical-align: middle;"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="description" style="margin-top: 10px;">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Link to licenses page */
|
||||
esc_html__('For more actions (revoke, extend, delete), go to the %s page.', 'wc-licensed-product'),
|
||||
'<a href="' . esc_url(admin_url('admin.php?page=wc-licenses')) . '">' . esc_html__('Licenses', 'wc-licensed-product') . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wclp-order-licenses { padding: 10px 0; }
|
||||
.wclp-order-domain-section { margin-bottom: 15px; }
|
||||
.wclp-inline-edit { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
||||
.wclp-inline-edit .spinner { float: none; margin: 0; }
|
||||
.wclp-status-message { font-style: italic; color: #666; }
|
||||
.wclp-status-message.success { color: #46b450; }
|
||||
.wclp-status-message.error { color: #dc3232; }
|
||||
.wclp-licenses-table { margin-top: 10px; }
|
||||
.wclp-licenses-table th, .wclp-licenses-table td { padding: 8px 10px; vertical-align: middle; }
|
||||
.wclp-license-key { font-size: 12px; }
|
||||
.wclp-license-domain-edit { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
|
||||
.wclp-license-domain-edit .spinner { float: none; margin: 0; }
|
||||
.wclp-domain-input { max-width: 200px; }
|
||||
.wclp-license-status { padding: 3px 8px; border-radius: 3px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
||||
.wclp-status-active { background: #d4edda; color: #155724; }
|
||||
.wclp-status-inactive { background: #fff3cd; color: #856404; }
|
||||
.wclp-status-expired { background: #f8d7da; color: #721c24; }
|
||||
.wclp-status-revoked { background: #d6d8db; color: #383d41; }
|
||||
.wclp-lifetime { color: #0073aa; font-weight: 500; }
|
||||
.wclp-edit-domain-btn { color: #0073aa; text-decoration: none; }
|
||||
.wclp-edit-domain-btn .dashicons { font-size: 16px; width: 16px; height: 16px; }
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin scripts
|
||||
*/
|
||||
public function enqueueScripts(string $hook): void
|
||||
{
|
||||
// Check if we're on an order edit page
|
||||
$screen = get_current_screen();
|
||||
if (!$screen) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isOrderEdit = in_array($screen->id, ['shop_order', 'woocommerce_page_wc-orders'], true)
|
||||
|| (isset($_GET['page']) && $_GET['page'] === 'wc-orders' && isset($_GET['action']) && $_GET['action'] === 'edit');
|
||||
|
||||
if (!$isOrderEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'wclp-order-licenses',
|
||||
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/order-licenses.js',
|
||||
['jquery'],
|
||||
WC_LICENSED_PRODUCT_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script('wclp-order-licenses', 'wclpOrderLicenses', [
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('wclp_order_license_actions'),
|
||||
'strings' => [
|
||||
'saving' => __('Saving...', 'wc-licensed-product'),
|
||||
'saved' => __('Saved!', 'wc-licensed-product'),
|
||||
'error' => __('Error saving. Please try again.', 'wc-licensed-product'),
|
||||
'invalidDomain' => __('Please enter a valid domain.', 'wc-licensed-product'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for updating order domain
|
||||
*/
|
||||
public function ajaxUpdateOrderDomain(): void
|
||||
{
|
||||
check_ajax_referer('wclp_order_license_actions', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$orderId = absint($_POST['order_id'] ?? 0);
|
||||
$domain = sanitize_text_field($_POST['domain'] ?? '');
|
||||
|
||||
if (!$orderId) {
|
||||
wp_send_json_error(['message' => __('Invalid order ID.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$order = wc_get_order($orderId);
|
||||
if (!$order) {
|
||||
wp_send_json_error(['message' => __('Order not found.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
// Normalize and validate domain
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
if (!empty($domain) && !$this->isValidDomain($normalizedDomain)) {
|
||||
wp_send_json_error(['message' => __('Invalid domain format.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
// Update order meta
|
||||
$order->update_meta_data('_licensed_product_domain', $normalizedDomain);
|
||||
$order->save();
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => __('Order domain updated.', 'wc-licensed-product'),
|
||||
'domain' => $normalizedDomain,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for updating license domain
|
||||
*/
|
||||
public function ajaxUpdateLicenseDomain(): void
|
||||
{
|
||||
check_ajax_referer('wclp_order_license_actions', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_woocommerce')) {
|
||||
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
$licenseId = absint($_POST['license_id'] ?? 0);
|
||||
$domain = sanitize_text_field($_POST['domain'] ?? '');
|
||||
|
||||
if (!$licenseId) {
|
||||
wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
if (empty($domain)) {
|
||||
wp_send_json_error(['message' => __('Domain cannot be empty.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
// Normalize and validate domain
|
||||
$normalizedDomain = $this->licenseManager->normalizeDomain($domain);
|
||||
if (!$this->isValidDomain($normalizedDomain)) {
|
||||
wp_send_json_error(['message' => __('Invalid domain format.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
// Get license to verify it exists
|
||||
$license = $this->licenseManager->getLicenseById($licenseId);
|
||||
if (!$license) {
|
||||
wp_send_json_error(['message' => __('License not found.', 'wc-licensed-product')]);
|
||||
}
|
||||
|
||||
// Update license domain
|
||||
$success = $this->licenseManager->updateLicenseDomain($licenseId, $normalizedDomain);
|
||||
|
||||
if ($success) {
|
||||
wp_send_json_success([
|
||||
'message' => __('License domain updated.', 'wc-licensed-product'),
|
||||
'domain' => $normalizedDomain,
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Failed to update license domain.', 'wc-licensed-product')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate domain format
|
||||
*/
|
||||
private function isValidDomain(string $domain): bool
|
||||
{
|
||||
if (empty($domain)) {
|
||||
return true; // Empty is allowed for order domain
|
||||
}
|
||||
|
||||
if (strlen($domain) > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = '/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/';
|
||||
return (bool) preg_match($pattern, $domain);
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,25 @@ class LicenseManager
|
||||
return $row ? License::fromArray($row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses for an order
|
||||
*/
|
||||
public function getLicensesByOrder(int $orderId): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} WHERE order_id = %d ORDER BY created_at DESC",
|
||||
$orderId
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses for a customer
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Jeremias\WcLicensedProduct;
|
||||
|
||||
use Jeremias\WcLicensedProduct\Admin\AdminController;
|
||||
use Jeremias\WcLicensedProduct\Admin\OrderLicenseController;
|
||||
use Jeremias\WcLicensedProduct\Admin\SettingsController;
|
||||
use Jeremias\WcLicensedProduct\Admin\VersionAdminController;
|
||||
use Jeremias\WcLicensedProduct\Api\RestApiController;
|
||||
@@ -122,6 +123,7 @@ final class Plugin
|
||||
if (is_admin()) {
|
||||
new AdminController($this->twig, $this->licenseManager);
|
||||
new VersionAdminController($this->versionManager);
|
||||
new OrderLicenseController($this->licenseManager);
|
||||
new SettingsController();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WooCommerce Licensed Product
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
|
||||
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
|
||||
* Version: 0.0.9
|
||||
* Version: 0.0.10
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL-2.0-or-later
|
||||
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// Plugin constants
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.0.9');
|
||||
define('WC_LICENSED_PRODUCT_VERSION', '0.0.10');
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
Reference in New Issue
Block a user