You've already forked wc-licensed-product
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:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user