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 %} + + +