diff --git a/CHANGELOG.md b/CHANGELOG.md index ddcea90..e501ab5 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/CLAUDE.md b/CLAUDE.md index 75e55f9..9d1a57d 100644 --- a/CLAUDE.md +++ b/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 diff --git a/assets/js/order-licenses.js b/assets/js/order-licenses.js new file mode 100644 index 0000000..13e13c0 --- /dev/null +++ b/assets/js/order-licenses.js @@ -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); diff --git a/src/Admin/OrderLicenseController.php b/src/Admin/OrderLicenseController.php new file mode 100644 index 0000000..829d999 --- /dev/null +++ b/src/Admin/OrderLicenseController.php @@ -0,0 +1,395 @@ +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 '
' . esc_html__('Order not found.', 'wc-licensed-product') . '
'; + 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 '' . esc_html__('This order does not contain licensed products.', 'wc-licensed-product') . '
'; + 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'); + ?> ++ +
+
+
+ is_paid()): ?>
+
+
+
+
+
+
+
| + | + | + | + | + | + |
|---|---|---|---|---|---|
+ getLicenseKey()); ?>
+ |
+ + + + get_name()); ?> + + + + + | +
+
+ getDomain()); ?>
+
+
+
+
+
+
+ |
+ + + getStatus())); ?> + + | ++ getExpiresAt(); + if ($expiresAt) { + echo esc_html($expiresAt->format(get_option('date_format'))); + } else { + echo '' . esc_html__('Lifetime', 'wc-licensed-product') . ''; + } + ?> + | ++ + + + | +
+ ' . esc_html__('Licenses', 'wc-licensed-product') . '' + ); + ?> +
+ +