Add inline editing for licenses and copy license key button

- Add inline editing for status, expiry date, and domain fields
- Add copy-to-clipboard button for license keys
- Add AJAX handlers for inline editing with nonce verification
- Update LicenseManager with updateLicenseExpiry method
- Add new translations for inline editing strings (de_CH)
- Compile updated German translations to .mo file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 23:13:07 +01:00
parent e88423e882
commit 024733bb31
8 changed files with 872 additions and 29 deletions

View File

@@ -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
<th scope="row" class="check-column">
<input type="checkbox" name="license_ids[]" value="<?php echo esc_attr($item['license']->getId()); ?>">
</th>
<td><code><?php echo esc_html($item['license']->getLicenseKey()); ?></code></td>
<td>
<code class="wclp-license-key"><?php echo esc_html($item['license']->getLicenseKey()); ?></code>
<button type="button" class="wclp-copy-btn button-link" data-license-key="<?php echo esc_attr($item['license']->getLicenseKey()); ?>" title="<?php esc_attr_e('Copy to clipboard', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-clipboard"></span>
</button>
</td>
<td>
<?php if ($item['product_edit_url']): ?>
<a href="<?php echo esc_url($item['product_edit_url']); ?>">
@@ -1105,21 +1289,55 @@ final class AdminController
<br><small><?php echo esc_html($item['customer_email']); ?></small>
<?php endif; ?>
</td>
<td><?php echo esc_html($item['license']->getDomain()); ?></td>
<td>
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
</span>
<td class="wclp-editable-cell" data-field="domain" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<span class="wclp-display-value"><?php echo esc_html($item['license']->getDomain()); ?></span>
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<input type="text" class="wclp-edit-input" value="<?php echo esc_attr($item['license']->getDomain()); ?>">
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</div>
</td>
<td>
<?php
$expiresAt = $item['license']->getExpiresAt();
if ($expiresAt) {
echo esc_html($expiresAt->format(get_option('date_format')));
} else {
echo '<span class="license-lifetime">' . esc_html__('Lifetime', 'wc-licensed-product') . '</span>';
}
?>
<td class="wclp-editable-cell" data-field="status" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<span class="wclp-display-value">
<span class="license-status license-status-<?php echo esc_attr($item['license']->getStatus()); ?>">
<?php echo esc_html(ucfirst($item['license']->getStatus())); ?>
</span>
</span>
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<select class="wclp-edit-input">
<option value="active" <?php selected($item['license']->getStatus(), 'active'); ?>><?php esc_html_e('Active', 'wc-licensed-product'); ?></option>
<option value="inactive" <?php selected($item['license']->getStatus(), 'inactive'); ?>><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></option>
<option value="expired" <?php selected($item['license']->getStatus(), 'expired'); ?>><?php esc_html_e('Expired', 'wc-licensed-product'); ?></option>
<option value="revoked" <?php selected($item['license']->getStatus(), 'revoked'); ?>><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></option>
</select>
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</div>
</td>
<td class="wclp-editable-cell" data-field="expiry" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<?php $expiresAt = $item['license']->getExpiresAt(); ?>
<span class="wclp-display-value">
<?php if ($expiresAt): ?>
<?php echo esc_html($expiresAt->format(get_option('date_format'))); ?>
<?php else: ?>
<span class="license-lifetime"><?php esc_html_e('Lifetime', 'wc-licensed-product'); ?></span>
<?php endif; ?>
</span>
<button type="button" class="wclp-edit-btn button-link" title="<?php esc_attr_e('Edit', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-edit"></span>
</button>
<div class="wclp-edit-form" style="display:none;">
<input type="date" class="wclp-edit-input" value="<?php echo $expiresAt ? esc_attr($expiresAt->format('Y-m-d')) : ''; ?>" placeholder="<?php esc_attr_e('Leave empty for lifetime', 'wc-licensed-product'); ?>">
<button type="button" class="wclp-save-btn button button-small button-primary"><?php esc_html_e('Save', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-cancel-btn button button-small"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
<button type="button" class="wclp-lifetime-btn button button-small" title="<?php esc_attr_e('Set to lifetime', 'wc-licensed-product'); ?>">∞</button>
</div>
</td>
<td class="license-actions">
<div class="row-actions">

View File

@@ -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
*/