diff --git a/assets/css/admin.css b/assets/css/admin.css
index 3cda32c..17537b7 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -579,3 +579,144 @@
#license-search-input {
width: 280px;
}
+
+/* Inline Editing Styles */
+.wclp-editable-cell {
+ position: relative;
+}
+
+.wclp-editable-cell .wclp-display-value {
+ display: inline;
+}
+
+.wclp-editable-cell .wclp-edit-btn {
+ opacity: 0;
+ transition: opacity 0.15s ease;
+ color: #2271b1;
+ padding: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+.wclp-editable-cell:hover .wclp-edit-btn {
+ opacity: 1;
+}
+
+.wclp-editable-cell .wclp-edit-btn .dashicons {
+ font-size: 14px;
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+}
+
+.wclp-editable-cell .wclp-edit-btn:hover {
+ color: #135e96;
+}
+
+.wclp-edit-form {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ flex-wrap: wrap;
+}
+
+.wclp-edit-form .wclp-edit-input {
+ max-width: 150px;
+ height: 28px;
+ padding: 0 8px;
+}
+
+.wclp-edit-form .wclp-edit-input[type="date"] {
+ max-width: 130px;
+}
+
+.wclp-edit-form select.wclp-edit-input {
+ max-width: 120px;
+}
+
+.wclp-edit-form .button-small {
+ height: 26px;
+ line-height: 24px;
+ padding: 0 8px;
+ font-size: 11px;
+}
+
+.wclp-edit-form .wclp-lifetime-btn {
+ font-size: 16px;
+ padding: 0 6px;
+}
+
+/* Inline notice animation */
+.wclp-inline-notice {
+ margin: 10px 0;
+}
+
+/* Make editable cells have a minimum height for consistency */
+.licenses-table .wclp-editable-cell {
+ min-width: 120px;
+}
+
+/* Domain column wider for edit form */
+.licenses-table td[data-field="domain"] {
+ min-width: 180px;
+}
+
+/* Status column */
+.licenses-table td[data-field="status"] {
+ min-width: 150px;
+}
+
+/* Expiry column */
+.licenses-table td[data-field="expiry"] {
+ min-width: 200px;
+}
+
+/* Copy License Key Button */
+.wclp-copy-btn {
+ color: #2271b1;
+ padding: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ position: relative;
+ cursor: pointer;
+}
+
+.wclp-copy-btn:hover {
+ color: #135e96;
+}
+
+.wclp-copy-btn .dashicons {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ vertical-align: middle;
+}
+
+.wclp-copy-tooltip {
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #1d2327;
+ color: #fff;
+ padding: 4px 8px;
+ border-radius: 3px;
+ font-size: 11px;
+ white-space: nowrap;
+ z-index: 100;
+ margin-bottom: 5px;
+}
+
+.wclp-copy-tooltip::after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border: 5px solid transparent;
+ border-top-color: #1d2327;
+}
+
+.wclp-license-key {
+ user-select: all;
+}
diff --git a/assets/js/admin-licenses.js b/assets/js/admin-licenses.js
index 3b866e8..8954c9f 100644
--- a/assets/js/admin-licenses.js
+++ b/assets/js/admin-licenses.js
@@ -234,4 +234,266 @@
// Initialize when document is ready
$(document).ready(init);
+ // ========================================
+ // Inline Editing Functionality
+ // ========================================
+
+ /**
+ * Initialize inline editing
+ */
+ function initInlineEditing() {
+ // Edit button click
+ $(document).on('click', '.wclp-edit-btn', function(e) {
+ e.preventDefault();
+ var $cell = $(this).closest('.wclp-editable-cell');
+ showEditForm($cell);
+ });
+
+ // Cancel button click
+ $(document).on('click', '.wclp-cancel-btn', function(e) {
+ e.preventDefault();
+ var $cell = $(this).closest('.wclp-editable-cell');
+ hideEditForm($cell);
+ });
+
+ // Save button click
+ $(document).on('click', '.wclp-save-btn', function(e) {
+ e.preventDefault();
+ var $cell = $(this).closest('.wclp-editable-cell');
+ saveEdit($cell);
+ });
+
+ // Lifetime button click (for expiry field)
+ $(document).on('click', '.wclp-lifetime-btn', function(e) {
+ e.preventDefault();
+ var $cell = $(this).closest('.wclp-editable-cell');
+ $cell.find('.wclp-edit-input').val('');
+ saveEdit($cell);
+ });
+
+ // Enter key saves, Escape cancels
+ $(document).on('keydown', '.wclp-edit-input', function(e) {
+ var $cell = $(this).closest('.wclp-editable-cell');
+ if (e.keyCode === 13) { // Enter
+ e.preventDefault();
+ saveEdit($cell);
+ } else if (e.keyCode === 27) { // Escape
+ e.preventDefault();
+ hideEditForm($cell);
+ }
+ });
+ }
+
+ /**
+ * Show edit form for a cell
+ */
+ function showEditForm($cell) {
+ $cell.find('.wclp-display-value, .wclp-edit-btn').hide();
+ $cell.find('.wclp-edit-form').show();
+ $cell.find('.wclp-edit-input').focus().select();
+ }
+
+ /**
+ * Hide edit form for a cell
+ */
+ function hideEditForm($cell) {
+ $cell.find('.wclp-edit-form').hide();
+ $cell.find('.wclp-display-value, .wclp-edit-btn').show();
+ }
+
+ /**
+ * Save edit via AJAX
+ */
+ function saveEdit($cell) {
+ var field = $cell.data('field');
+ var licenseId = $cell.data('license-id');
+ var $input = $cell.find('.wclp-edit-input');
+ var value = $input.val();
+ var $saveBtn = $cell.find('.wclp-save-btn');
+ var originalText = $saveBtn.text();
+
+ // Determine action based on field
+ var action;
+ var data = {
+ action: '',
+ nonce: wclpAdmin.editNonce,
+ license_id: licenseId
+ };
+
+ switch (field) {
+ case 'status':
+ data.action = 'wclp_update_license_status';
+ data.status = value;
+ break;
+ case 'expiry':
+ data.action = 'wclp_update_license_expiry';
+ data.expiry_date = value;
+ break;
+ case 'domain':
+ data.action = 'wclp_update_license_domain';
+ data.domain = value;
+ break;
+ default:
+ return;
+ }
+
+ // Show saving state
+ $saveBtn.text(wclpAdmin.strings.saving).prop('disabled', true);
+ $cell.find('.wclp-cancel-btn, .wclp-lifetime-btn').prop('disabled', true);
+
+ $.ajax({
+ url: wclpAdmin.ajaxUrl,
+ type: 'POST',
+ data: data,
+ success: function(response) {
+ if (response.success) {
+ // Update display value
+ updateDisplayValue($cell, field, response.data);
+ hideEditForm($cell);
+ showNotice('success', response.data.message);
+ } else {
+ showNotice('error', response.data.message || wclpAdmin.strings.saveFailed);
+ }
+ },
+ error: function() {
+ showNotice('error', wclpAdmin.strings.saveFailed);
+ },
+ complete: function() {
+ $saveBtn.text(originalText).prop('disabled', false);
+ $cell.find('.wclp-cancel-btn, .wclp-lifetime-btn').prop('disabled', false);
+ }
+ });
+ }
+
+ /**
+ * Update the display value after successful save
+ */
+ function updateDisplayValue($cell, field, data) {
+ var $display = $cell.find('.wclp-display-value');
+
+ switch (field) {
+ case 'status':
+ var statusHtml = '' +
+ escapeHtml(data.status_label) + '';
+ $display.html(statusHtml);
+ break;
+ case 'expiry':
+ if (data.expiry_date) {
+ $display.html(escapeHtml(data.expiry_display));
+ } else {
+ $display.html('' + wclpAdmin.strings.lifetime + '');
+ }
+ // Update the input value
+ $cell.find('.wclp-edit-input').val(data.expiry_date || '');
+ break;
+ case 'domain':
+ $display.text(data.domain);
+ // Update the input value
+ $cell.find('.wclp-edit-input').val(data.domain);
+ break;
+ }
+ }
+
+ /**
+ * Show a temporary notice
+ */
+ function showNotice(type, message) {
+ var $notice = $('
' +
+ escapeHtml(message) + '
');
+
+ // Insert at the top of the wrap
+ $('.wrap h1').first().after($notice);
+
+ // Auto-dismiss after 3 seconds
+ setTimeout(function() {
+ $notice.fadeOut(300, function() {
+ $(this).remove();
+ });
+ }, 3000);
+ }
+
+ // Initialize inline editing when document is ready
+ $(document).ready(initInlineEditing);
+
+ // ========================================
+ // Copy License Key Functionality
+ // ========================================
+
+ /**
+ * Initialize copy license key buttons
+ */
+ function initCopyButtons() {
+ $(document).on('click', '.wclp-copy-btn', function(e) {
+ e.preventDefault();
+ var $btn = $(this);
+ var licenseKey = $btn.data('license-key');
+
+ copyToClipboard(licenseKey).then(function() {
+ showCopyFeedback($btn, true);
+ }).catch(function() {
+ showCopyFeedback($btn, false);
+ });
+ });
+ }
+
+ /**
+ * Copy text to clipboard
+ */
+ function copyToClipboard(text) {
+ // Modern Clipboard API
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ return navigator.clipboard.writeText(text);
+ }
+
+ // Fallback for older browsers
+ return new Promise(function(resolve, reject) {
+ var textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.style.position = 'fixed';
+ textarea.style.opacity = '0';
+ document.body.appendChild(textarea);
+ textarea.select();
+
+ try {
+ var successful = document.execCommand('copy');
+ document.body.removeChild(textarea);
+ if (successful) {
+ resolve();
+ } else {
+ reject();
+ }
+ } catch (err) {
+ document.body.removeChild(textarea);
+ reject(err);
+ }
+ });
+ }
+
+ /**
+ * Show copy feedback
+ */
+ function showCopyFeedback($btn, success) {
+ var $icon = $btn.find('.dashicons');
+ var originalClass = 'dashicons-clipboard';
+ var feedbackClass = success ? 'dashicons-yes' : 'dashicons-no';
+ var feedbackColor = success ? '#00a32a' : '#d63638';
+
+ // Change icon temporarily
+ $icon.removeClass(originalClass).addClass(feedbackClass).css('color', feedbackColor);
+
+ // Show tooltip
+ var message = success ? wclpAdmin.strings.copied : wclpAdmin.strings.copyFailed;
+ var $tooltip = $('' + escapeHtml(message) + '');
+ $btn.append($tooltip);
+
+ // Reset after delay
+ setTimeout(function() {
+ $icon.removeClass(feedbackClass).addClass(originalClass).css('color', '');
+ $tooltip.remove();
+ }, 1500);
+ }
+
+ // Initialize copy buttons when document is ready
+ $(document).ready(initCopyButtons);
+
})(jQuery);
diff --git a/languages/wc-licensed-product-de_CH.mo b/languages/wc-licensed-product-de_CH.mo
index 1a2b486..3d6cb54 100644
Binary files a/languages/wc-licensed-product-de_CH.mo and b/languages/wc-licensed-product-de_CH.mo differ
diff --git a/languages/wc-licensed-product-de_CH.po b/languages/wc-licensed-product-de_CH.po
index de41c00..a4f203e 100644
--- a/languages/wc-licensed-product-de_CH.po
+++ b/languages/wc-licensed-product-de_CH.po
@@ -3,7 +3,7 @@
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
-"Project-Id-Version: WC Licensed Product 0.0.8\n"
+"Project-Id-Version: WC Licensed Product 0.0.10\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n"
"POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
"PO-Revision-Date: 2026-01-21T00:00:00+00:00\n"
@@ -865,3 +865,77 @@ msgstr "E-Mail-Typ"
msgid "Choose which format of email to send."
msgstr "Wählen Sie, welches E-Mail-Format gesendet werden soll."
+
+#. Admin - Inline Editing
+msgid "Searching..."
+msgstr "Suche..."
+
+msgid "No results found"
+msgstr "Keine Ergebnisse gefunden"
+
+msgid "Saving..."
+msgstr "Speichere..."
+
+msgid "Saved"
+msgstr "Gespeichert"
+
+msgid "Save failed"
+msgstr "Speichern fehlgeschlagen"
+
+msgid "Are you sure you want to revoke this license? This action cannot be undone."
+msgstr "Sind Sie sicher, dass Sie diese Lizenz widerrufen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
+
+msgid "Edit"
+msgstr "Bearbeiten"
+
+msgid "Save"
+msgstr "Speichern"
+
+msgid "Status updated successfully."
+msgstr "Status erfolgreich aktualisiert."
+
+msgid "Expiry date updated successfully."
+msgstr "Ablaufdatum erfolgreich aktualisiert."
+
+msgid "License set to lifetime."
+msgstr "Lizenz auf lebenslang gesetzt."
+
+msgid "Domain updated successfully."
+msgstr "Domain erfolgreich aktualisiert."
+
+msgid "Invalid license ID."
+msgstr "Ungültige Lizenz-ID."
+
+msgid "Invalid status."
+msgstr "Ungültiger Status."
+
+msgid "Invalid date format."
+msgstr "Ungültiges Datumsformat."
+
+msgid "Domain cannot be empty."
+msgstr "Domain darf nicht leer sein."
+
+msgid "Failed to update status."
+msgstr "Status konnte nicht aktualisiert werden."
+
+msgid "Failed to update expiry date."
+msgstr "Ablaufdatum konnte nicht aktualisiert werden."
+
+msgid "Failed to update domain."
+msgstr "Domain konnte nicht aktualisiert werden."
+
+msgid "Failed to revoke license."
+msgstr "Lizenz konnte nicht widerrufen werden."
+
+msgid "Leave empty for lifetime"
+msgstr "Leer lassen für lebenslang"
+
+#. Admin - Order License Management
+msgid "Saved!"
+msgstr "Gespeichert!"
+
+msgid "Error saving. Please try again."
+msgstr "Fehler beim Speichern. Bitte versuchen Sie es erneut."
+
+msgid "Failed to update license domain."
+msgstr "Lizenz-Domain konnte nicht aktualisiert werden."
diff --git a/languages/wc-licensed-product.pot b/languages/wc-licensed-product.pot
index 30fe460..3fe9de9 100644
--- a/languages/wc-licensed-product.pot
+++ b/languages/wc-licensed-product.pot
@@ -2,7 +2,7 @@
# This file is distributed under the GPL-2.0-or-later.
msgid ""
msgstr ""
-"Project-Id-Version: WC Licensed Product 0.0.8\n"
+"Project-Id-Version: WC Licensed Product 0.0.10\n"
"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wc-licensed-product/issues\n"
"POT-Creation-Date: 2026-01-21T00:00:00+00:00\n"
"MIME-Version: 1.0\n"
@@ -865,3 +865,77 @@ msgstr ""
msgid "Customer"
msgstr ""
+
+#. Admin - Inline Editing
+msgid "Searching..."
+msgstr ""
+
+msgid "No results found"
+msgstr ""
+
+msgid "Saving..."
+msgstr ""
+
+msgid "Saved"
+msgstr ""
+
+msgid "Save failed"
+msgstr ""
+
+msgid "Are you sure you want to revoke this license? This action cannot be undone."
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
+msgid "Status updated successfully."
+msgstr ""
+
+msgid "Expiry date updated successfully."
+msgstr ""
+
+msgid "License set to lifetime."
+msgstr ""
+
+msgid "Domain updated successfully."
+msgstr ""
+
+msgid "Invalid license ID."
+msgstr ""
+
+msgid "Invalid status."
+msgstr ""
+
+msgid "Invalid date format."
+msgstr ""
+
+msgid "Domain cannot be empty."
+msgstr ""
+
+msgid "Failed to update status."
+msgstr ""
+
+msgid "Failed to update expiry date."
+msgstr ""
+
+msgid "Failed to update domain."
+msgstr ""
+
+msgid "Failed to revoke license."
+msgstr ""
+
+msgid "Leave empty for lifetime"
+msgstr ""
+
+#. Admin - Order License Management
+msgid "Saved!"
+msgstr ""
+
+msgid "Error saving. Please try again."
+msgstr ""
+
+msgid "Failed to update license domain."
+msgstr ""
diff --git a/src/Admin/AdminController.php b/src/Admin/AdminController.php
index 702b9b9..6251646 100644
--- a/src/Admin/AdminController.php
+++ b/src/Admin/AdminController.php
@@ -55,6 +55,12 @@ final class AdminController
// AJAX handler for live search
add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']);
+
+ // AJAX handlers for inline editing
+ add_action('wp_ajax_wclp_update_license_status', [$this, 'handleAjaxStatusUpdate']);
+ add_action('wp_ajax_wclp_update_license_expiry', [$this, 'handleAjaxExpiryUpdate']);
+ add_action('wp_ajax_wclp_update_license_domain', [$this, 'handleAjaxDomainUpdate']);
+ add_action('wp_ajax_wclp_revoke_license', [$this, 'handleAjaxRevoke']);
}
/**
@@ -125,10 +131,27 @@ final class AdminController
wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wclp_live_search'),
+ 'editNonce' => wp_create_nonce('wclp_inline_edit'),
'strings' => [
'noResults' => __('No licenses found', 'wc-licensed-product'),
'searching' => __('Searching...', 'wc-licensed-product'),
'error' => __('Search failed', 'wc-licensed-product'),
+ 'saving' => __('Saving...', 'wc-licensed-product'),
+ 'saved' => __('Saved', 'wc-licensed-product'),
+ 'saveFailed' => __('Save failed', 'wc-licensed-product'),
+ 'confirmRevoke' => __('Are you sure you want to revoke this license? This action cannot be undone.', 'wc-licensed-product'),
+ 'edit' => __('Edit', 'wc-licensed-product'),
+ 'cancel' => __('Cancel', 'wc-licensed-product'),
+ 'save' => __('Save', 'wc-licensed-product'),
+ 'lifetime' => __('Lifetime', 'wc-licensed-product'),
+ 'copied' => __('Copied!', 'wc-licensed-product'),
+ 'copyFailed' => __('Copy failed', 'wc-licensed-product'),
+ ],
+ 'statuses' => [
+ ['value' => 'active', 'label' => __('Active', 'wc-licensed-product')],
+ ['value' => 'inactive', 'label' => __('Inactive', 'wc-licensed-product')],
+ ['value' => 'expired', 'label' => __('Expired', 'wc-licensed-product')],
+ ['value' => 'revoked', 'label' => __('Revoked', 'wc-licensed-product')],
],
]);
}
@@ -174,6 +197,162 @@ final class AdminController
wp_send_json_success(['results' => $results]);
}
+ /**
+ * Handle AJAX status update
+ */
+ public function handleAjaxStatusUpdate(): void
+ {
+ check_ajax_referer('wclp_inline_edit', 'nonce');
+
+ if (!current_user_can('manage_woocommerce')) {
+ wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
+ }
+
+ $licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
+ $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
+
+ if (!$licenseId) {
+ wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
+ }
+
+ $validStatuses = [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_EXPIRED, License::STATUS_REVOKED];
+ if (!in_array($status, $validStatuses, true)) {
+ wp_send_json_error(['message' => __('Invalid status.', 'wc-licensed-product')]);
+ }
+
+ $success = $this->licenseManager->updateLicenseStatus($licenseId, $status);
+
+ if ($success) {
+ wp_send_json_success([
+ 'message' => __('Status updated successfully.', 'wc-licensed-product'),
+ 'status' => $status,
+ 'status_label' => ucfirst($status),
+ ]);
+ } else {
+ wp_send_json_error(['message' => __('Failed to update status.', 'wc-licensed-product')]);
+ }
+ }
+
+ /**
+ * Handle AJAX expiry date update
+ */
+ public function handleAjaxExpiryUpdate(): void
+ {
+ check_ajax_referer('wclp_inline_edit', 'nonce');
+
+ if (!current_user_can('manage_woocommerce')) {
+ wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
+ }
+
+ $licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
+ $expiryDate = isset($_POST['expiry_date']) ? sanitize_text_field($_POST['expiry_date']) : '';
+
+ if (!$licenseId) {
+ wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
+ }
+
+ // Handle "lifetime" option
+ if (empty($expiryDate) || strtolower($expiryDate) === 'lifetime') {
+ $success = $this->licenseManager->setLicenseLifetime($licenseId);
+ if ($success) {
+ wp_send_json_success([
+ 'message' => __('License set to lifetime.', 'wc-licensed-product'),
+ 'expiry_date' => '',
+ 'expiry_display' => __('Lifetime', 'wc-licensed-product'),
+ ]);
+ } else {
+ wp_send_json_error(['message' => __('Failed to update expiry date.', 'wc-licensed-product')]);
+ }
+ return;
+ }
+
+ // Validate date format
+ try {
+ $date = new \DateTimeImmutable($expiryDate);
+ $success = $this->licenseManager->updateLicenseExpiry($licenseId, $date);
+
+ if ($success) {
+ wp_send_json_success([
+ 'message' => __('Expiry date updated successfully.', 'wc-licensed-product'),
+ 'expiry_date' => $date->format('Y-m-d'),
+ 'expiry_display' => $date->format(get_option('date_format')),
+ ]);
+ } else {
+ wp_send_json_error(['message' => __('Failed to update expiry date.', 'wc-licensed-product')]);
+ }
+ } catch (\Exception $e) {
+ wp_send_json_error(['message' => __('Invalid date format.', 'wc-licensed-product')]);
+ }
+ }
+
+ /**
+ * Handle AJAX domain update
+ */
+ public function handleAjaxDomainUpdate(): void
+ {
+ check_ajax_referer('wclp_inline_edit', 'nonce');
+
+ if (!current_user_can('manage_woocommerce')) {
+ wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
+ }
+
+ $licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
+ $domain = isset($_POST['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')]);
+ }
+
+ $success = $this->licenseManager->transferLicense($licenseId, $domain);
+
+ if ($success) {
+ // Get the normalized domain from the license
+ $license = $this->licenseManager->getLicenseById($licenseId);
+ $normalizedDomain = $license ? $license->getDomain() : $domain;
+
+ wp_send_json_success([
+ 'message' => __('Domain updated successfully.', 'wc-licensed-product'),
+ 'domain' => $normalizedDomain,
+ ]);
+ } else {
+ wp_send_json_error(['message' => __('Failed to update domain.', 'wc-licensed-product')]);
+ }
+ }
+
+ /**
+ * Handle AJAX revoke
+ */
+ public function handleAjaxRevoke(): void
+ {
+ check_ajax_referer('wclp_inline_edit', 'nonce');
+
+ if (!current_user_can('manage_woocommerce')) {
+ wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
+ }
+
+ $licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
+
+ if (!$licenseId) {
+ wp_send_json_error(['message' => __('Invalid license ID.', 'wc-licensed-product')]);
+ }
+
+ $success = $this->licenseManager->updateLicenseStatus($licenseId, License::STATUS_REVOKED);
+
+ if ($success) {
+ wp_send_json_success([
+ 'message' => __('License revoked successfully.', 'wc-licensed-product'),
+ 'status' => License::STATUS_REVOKED,
+ 'status_label' => ucfirst(License::STATUS_REVOKED),
+ ]);
+ } else {
+ wp_send_json_error(['message' => __('Failed to revoke license.', 'wc-licensed-product')]);
+ }
+ }
+
/**
* Handle admin actions (update, delete licenses)
*/
@@ -1089,7 +1268,12 @@ final class AdminController
|
- getLicenseKey()); ?> |
+
+ getLicenseKey()); ?>
+
+ |
@@ -1105,21 +1289,55 @@ final class AdminController
|
- getDomain()); ?> |
-
-
- getStatus())); ?>
-
+ |
+ getDomain()); ?>
+
+
+
+
+
+
|
-
- getExpiresAt();
- if ($expiresAt) {
- echo esc_html($expiresAt->format(get_option('date_format')));
- } else {
- echo '' . esc_html__('Lifetime', 'wc-licensed-product') . '';
- }
- ?>
+ |
+
+
+ getStatus())); ?>
+
+
+
+
+
+
+
+
+ |
+
+ getExpiresAt(); ?>
+
+
+ format(get_option('date_format'))); ?>
+
+
+
+
+
+
+
+
+
+
+
|
diff --git a/src/License/LicenseManager.php b/src/License/LicenseManager.php
index c06b798..4645e6a 100644
--- a/src/License/LicenseManager.php
+++ b/src/License/LicenseManager.php
@@ -424,6 +424,39 @@ class LicenseManager
return $result !== false;
}
+ /**
+ * Update license expiry date
+ *
+ * @param int $licenseId License ID
+ * @param \DateTimeImmutable $expiresAt New expiry date
+ * @return bool Success
+ */
+ public function updateLicenseExpiry(int $licenseId, \DateTimeImmutable $expiresAt): bool
+ {
+ global $wpdb;
+
+ $license = $this->getLicenseById($licenseId);
+ if (!$license) {
+ return false;
+ }
+
+ $tableName = Installer::getLicensesTable();
+ $result = $wpdb->update(
+ $tableName,
+ ['expires_at' => $expiresAt->format('Y-m-d H:i:s')],
+ ['id' => $licenseId],
+ ['%s'],
+ ['%d']
+ );
+
+ // If license was expired and new date is in the future, reactivate it
+ if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED && $expiresAt > new \DateTimeImmutable()) {
+ $this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
+ }
+
+ return $result !== false;
+ }
+
/**
* Update license domain
*/
diff --git a/templates/admin/licenses.html.twig b/templates/admin/licenses.html.twig
index f4adea6..ce7a9f3 100644
--- a/templates/admin/licenses.html.twig
+++ b/templates/admin/licenses.html.twig
@@ -110,7 +110,12 @@
|
- {{ item.license.licenseKey }} |
+
+ {{ item.license.licenseKey }}
+
+ |
{% if item.product_edit_url %}
{{ esc_html(item.product_name) }}
@@ -124,18 +129,54 @@
{{ esc_html(item.customer_email) }}
{% endif %}
|
- {{ esc_html(item.license.domain) }} |
-
-
- {{ item.license.status|capitalize }}
-
+ |
+ {{ esc_html(item.license.domain) }}
+
+
+
+
+
+
|
-
- {% if item.license.expiresAt %}
- {{ item.license.expiresAt|date('Y-m-d') }}
- {% else %}
- {{ __('Lifetime') }}
- {% endif %}
+ |
+
+
+ {{ item.license.status|capitalize }}
+
+
+
+
+
+
+
+
+ |
+
+
+ {% if item.license.expiresAt %}
+ {{ item.license.expiresAt|date('Y-m-d') }}
+ {% else %}
+ {{ __('Lifetime') }}
+ {% endif %}
+
+
+
+
+
+
+
+
|
| |