Files
wc-licensed-product/templates/admin/licenses.html.twig
magdev 8b87c954eb Add license test action to admin overview
Added a "Test" action button in the license overview that validates
licenses against the /validate REST API endpoint. Results are shown
in a modal with validation status, error codes, and license details.

- Added Test link in row actions for each license
- Created AJAX handler handleAjaxTestLicense() in AdminController
- Added test result modal with loading state and result display
- Shows valid/invalid status with detailed error information

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 11:37:06 +01:00

477 lines
25 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="wrap">
<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 %}
<div class="notice notice-{{ notice.type }} is-dismissible">
<p>{{ esc_html(notice.message) }}</p>
</div>
{% endfor %}
<!-- Search and Filter Form -->
<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>
<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>
<tr>
<td class="manage-column column-cb check-column">
<input type="checkbox" id="cb-select-all-1">
</td>
<th>{{ __('License Key') }}</th>
<th>{{ __('Product') }}</th>
<th>{{ __('Customer') }}</th>
<th>{{ __('Domain') }}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Created') }}</th>
<th>{{ __('Expires') }}</th>
<th>{{ __('Actions') }}</th>
</tr>
</thead>
<tbody>
{% if licenses is empty %}
<tr>
<td colspan="9">{{ __('No licenses found.') }}</td>
</tr>
{% else %}
{% for item in licenses %}
<tr>
<th scope="row" class="check-column">
<input type="checkbox" name="license_ids[]" value="{{ item.license.id }}">
</th>
<td>
<code class="wclp-license-key">{{ item.license.licenseKey }}</code>
<button type="button" class="wclp-copy-btn button-link" data-license-key="{{ esc_attr(item.license.licenseKey) }}" title="{{ __('Copy to clipboard') }}">
<span class="dashicons dashicons-clipboard"></span>
</button>
</td>
<td>
{% if item.product_edit_url %}
<a href="{{ esc_url(item.product_edit_url) }}">{{ esc_html(item.product_name) }}</a>
{% else %}
{{ esc_html(item.product_name) }}
{% endif %}
</td>
<td>
{{ esc_html(item.customer_name) }}
{% if item.customer_email %}
<br><small>{{ esc_html(item.customer_email) }}</small>
{% endif %}
</td>
<td class="wclp-editable-cell" data-field="domain" data-license-id="{{ item.license.id }}">
<span class="wclp-display-value">{{ esc_html(item.license.domain) }}</span>
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<input type="text" class="wclp-edit-input" value="{{ esc_attr(item.license.domain) }}">
<button type="button" class="wclp-save-btn button button-small button-primary">{{ __('Save') }}</button>
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
</div>
</td>
<td class="wclp-editable-cell" data-field="status" data-license-id="{{ item.license.id }}">
<span class="wclp-display-value">
<span class="license-status license-status-{{ item.license.status }}">
{{ item.license.status|capitalize }}
</span>
</span>
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<select class="wclp-edit-input">
<option value="active" {{ item.license.status == 'active' ? 'selected' : '' }}>{{ __('Active') }}</option>
<option value="inactive" {{ item.license.status == 'inactive' ? 'selected' : '' }}>{{ __('Inactive') }}</option>
<option value="expired" {{ item.license.status == 'expired' ? 'selected' : '' }}>{{ __('Expired') }}</option>
<option value="revoked" {{ item.license.status == 'revoked' ? 'selected' : '' }}>{{ __('Revoked') }}</option>
</select>
<button type="button" class="wclp-save-btn button button-small button-primary">{{ __('Save') }}</button>
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
</div>
</td>
<td class="wclp-created-cell">
{{ item.license.createdAt|date('Y-m-d') }}
</td>
<td class="wclp-editable-cell" data-field="expiry" data-license-id="{{ item.license.id }}">
<span class="wclp-display-value">
{% if item.license.expiresAt %}
{{ item.license.expiresAt|date('Y-m-d') }}
{% else %}
<span class="license-lifetime">{{ __('Lifetime') }}</span>
{% endif %}
</span>
<button type="button" class="wclp-edit-btn button-link" title="{{ __('Edit') }}">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<input type="date" class="wclp-edit-input" value="{{ item.license.expiresAt ? item.license.expiresAt|date('Y-m-d') : '' }}" placeholder="{{ __('Leave empty for lifetime') }}">
<button type="button" class="wclp-save-btn button button-small button-primary">{{ __('Save') }}</button>
<button type="button" class="wclp-cancel-btn button button-small">{{ __('Cancel') }}</button>
<button type="button" class="wclp-lifetime-btn button button-small" title="{{ __('Set to lifetime') }}">∞</button>
</div>
</td>
<td class="license-actions">
<div class="row-actions">
<span class="test">
<a href="#" class="wclp-test-license-link"
data-license-id="{{ item.license.id }}"
data-license-key="{{ esc_attr(item.license.licenseKey) }}"
data-domain="{{ esc_attr(item.license.domain) }}"
title="{{ __('Test license against API') }}">{{ __('Test') }}</a> |
</span>
{% if item.license.status != 'revoked' %}
<span class="transfer">
<a href="#" class="wclp-transfer-link"
data-license-id="{{ item.license.id }}"
data-current-domain="{{ esc_attr(item.license.domain) }}"
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 %}
<span class="delete">
<a href="{{ delete_url(item.license.id) }}"
class="submitdelete"
onclick="return confirm('{{ __('Are you sure you want to delete this license?') }}')">{{ __('Delete') }}</a>
</span>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</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>{{ __('Created') }}</th>
<th>{{ __('Expires') }}</th>
<th>{{ __('Actions') }}</th>
</tr>
</tfoot>
</table>
<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">
<span class="pagination-links">
{% if current_page > 1 %}
<a class="prev-page button" href="{{ admin_url ~ '&paged=' ~ (current_page - 1) ~ filter_params }}">
<span aria-hidden="true"></span>
</a>
{% endif %}
<span class="paging-input">
{{ current_page }} {{ __('of') }} {{ total_pages }}
</span>
{% if current_page < total_pages %}
<a class="next-page button" href="{{ admin_url ~ '&paged=' ~ (current_page + 1) ~ filter_params }}">
<span aria-hidden="true"></span>
</a>
{% endif %}
</span>
</div>
{% endif %}
</div>
</form>
</div>
<!-- Test License Modal -->
<div id="wclp-test-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</span>
<h2>{{ __('License Validation Test') }}</h2>
<div class="wclp-test-info">
<table class="form-table">
<tr>
<th scope="row">{{ __('License Key') }}</th>
<td><code id="test-license-key"></code></td>
</tr>
<tr>
<th scope="row">{{ __('Domain') }}</th>
<td><code id="test-domain"></code></td>
</tr>
</table>
</div>
<div id="wclp-test-loading" style="display:none; text-align:center; padding:20px;">
<span class="spinner is-active" style="float:none;"></span>
<p>{{ __('Testing license...') }}</p>
</div>
<div id="wclp-test-result" style="display:none;">
<div id="wclp-test-result-content"></div>
</div>
<p class="submit">
<button type="button" class="button wclp-modal-cancel">{{ __('Close') }}</button>
</p>
</div>
</div>
<!-- Transfer Modal -->
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-content">
<span class="wclp-modal-close">&times;</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();
}
});
// Test License modal
var $testModal = $('#wclp-test-modal');
var $testLoading = $('#wclp-test-loading');
var $testResult = $('#wclp-test-result');
var $testResultContent = $('#wclp-test-result-content');
$('.wclp-test-license-link').on('click', function(e) {
e.preventDefault();
var licenseKey = $(this).data('license-key');
var domain = $(this).data('domain');
// Show modal with info
$('#test-license-key').text(licenseKey);
$('#test-domain').text(domain);
$testLoading.show();
$testResult.hide();
$testModal.show();
// Call the test endpoint
$.ajax({
url: wclpAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'wclp_test_license',
nonce: wclpAdmin.editNonce,
license_key: licenseKey,
domain: domain
},
success: function(response) {
$testLoading.hide();
if (response.success) {
var result = response.data;
var html = '';
if (result.valid) {
html = '<div class="notice notice-success inline"><p><strong>✓ {{ __('License is VALID') }}</strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th>{{ __('Product') }}</th><td>' + escapeHtml(result.product_name || '-') + '</td></tr>';
html += '<tr><th>{{ __('Version') }}</th><td>' + escapeHtml(result.version || '-') + '</td></tr>';
if (result.expires_at) {
html += '<tr><th>{{ __('Expires') }}</th><td>' + escapeHtml(result.expires_at) + '</td></tr>';
} else {
html += '<tr><th>{{ __('Expires') }}</th><td>{{ __('Lifetime') }}</td></tr>';
}
html += '</tbody></table>';
} else {
html = '<div class="notice notice-error inline"><p><strong>✗ {{ __('License is INVALID') }}</strong></p></div>';
html += '<table class="widefat striped"><tbody>';
html += '<tr><th>{{ __('Error Code') }}</th><td><code>' + escapeHtml(result.error || 'unknown') + '</code></td></tr>';
html += '<tr><th>{{ __('Message') }}</th><td>' + escapeHtml(result.message || '-') + '</td></tr>';
html += '</tbody></table>';
}
$testResultContent.html(html);
$testResult.show();
} else {
$testResultContent.html('<div class="notice notice-error inline"><p>' + escapeHtml(response.data.message || 'Error') + '</p></div>');
$testResult.show();
}
},
error: function() {
$testLoading.hide();
$testResultContent.html('<div class="notice notice-error inline"><p>{{ __('Failed to test license. Please try again.') }}</p></div>');
$testResult.show();
}
});
});
// Close test modal
$testModal.find('.wclp-modal-close, .wclp-modal-cancel').on('click', function() {
$testModal.hide();
});
$(window).on('click', function(e) {
if ($(e.target).is($testModal)) {
$testModal.hide();
}
});
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})(jQuery);
</script>