Implement versions 0.0.4-0.0.7 features

v0.0.4:
- Add WooCommerce settings tab for default license settings
- Per-product settings override global defaults

v0.0.5:
- Add bulk license operations (activate, deactivate, revoke, extend, delete)
- Add license renewal/extension and lifetime functionality
- Add quick action buttons per license row

v0.0.6:
- Add license dashboard with statistics and analytics
- Add license transfer functionality (admin)
- Add CSV export for licenses
- Add OpenAPI 3.1 specification
- Remove /deactivate API endpoint

v0.0.7:
- Move license dashboard to WooCommerce Reports section
- Add license search and filtering in admin
- Add customer-facing license transfer with AJAX modal
- Add email notifications for license expiration warnings
- Add bulk import licenses from CSV
- Update README with comprehensive documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 20:32:35 +01:00
parent 78e43b9aea
commit 49a0699963
21 changed files with 4132 additions and 289 deletions

View File

@@ -52,6 +52,9 @@ final class AccountController
// Enqueue frontend styles and scripts
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
// AJAX handler for license transfer request
add_action('wp_ajax_wclp_customer_transfer_license', [$this, 'handleTransferRequest']);
}
/**
@@ -181,7 +184,19 @@ final class AccountController
</div>
<div class="license-info-row">
<span><strong><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></strong> <?php echo esc_html($item['license']->getDomain()); ?></span>
<span class="license-domain-display" data-license-id="<?php echo esc_attr($item['license']->getId()); ?>">
<strong><?php esc_html_e('Domain:', 'wc-licensed-product'); ?></strong>
<span class="domain-value"><?php echo esc_html($item['license']->getDomain()); ?></span>
<?php if (in_array($item['license']->getStatus(), ['active', 'inactive'], true)): ?>
<button type="button" class="wclp-transfer-btn"
data-license-id="<?php echo esc_attr($item['license']->getId()); ?>"
data-current-domain="<?php echo esc_attr($item['license']->getDomain()); ?>"
title="<?php esc_attr_e('Transfer to new domain', 'wc-licensed-product'); ?>">
<span class="dashicons dashicons-randomize"></span>
<?php esc_html_e('Transfer', 'wc-licensed-product'); ?>
</button>
<?php endif; ?>
</span>
<span><strong><?php esc_html_e('Expires:', 'wc-licensed-product'); ?></strong>
<?php
$expiresAt = $item['license']->getExpiresAt();
@@ -213,6 +228,40 @@ final class AccountController
</div>
<?php endforeach; ?>
</div>
<!-- Transfer Modal -->
<div id="wclp-transfer-modal" class="wclp-modal" style="display:none;">
<div class="wclp-modal-overlay"></div>
<div class="wclp-modal-content">
<button type="button" class="wclp-modal-close" aria-label="<?php esc_attr_e('Close', 'wc-licensed-product'); ?>">&times;</button>
<h3><?php esc_html_e('Transfer License to New Domain', 'wc-licensed-product'); ?></h3>
<form id="wclp-transfer-form">
<input type="hidden" name="license_id" id="transfer-license-id" value="">
<div class="wclp-form-row">
<label><?php esc_html_e('Current Domain', 'wc-licensed-product'); ?></label>
<p class="wclp-current-domain"><code id="transfer-current-domain"></code></p>
</div>
<div class="wclp-form-row">
<label for="transfer-new-domain"><?php esc_html_e('New Domain', 'wc-licensed-product'); ?></label>
<input type="text" name="new_domain" id="transfer-new-domain"
placeholder="example.com" required
pattern="[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+">
<p class="wclp-field-description"><?php esc_html_e('Enter the new domain without http:// or www.', 'wc-licensed-product'); ?></p>
</div>
<div class="wclp-form-row wclp-form-actions">
<button type="submit" class="button wclp-btn-primary" id="wclp-transfer-submit">
<?php esc_html_e('Transfer License', 'wc-licensed-product'); ?>
</button>
<button type="button" class="button wclp-modal-cancel"><?php esc_html_e('Cancel', 'wc-licensed-product'); ?></button>
</div>
<div id="wclp-transfer-message" class="wclp-message" style="display:none;"></div>
</form>
</div>
</div>
<?php
}
@@ -241,10 +290,111 @@ final class AccountController
);
wp_localize_script('wc-licensed-product-frontend', 'wcLicensedProduct', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'transferNonce' => wp_create_nonce('wclp_customer_transfer'),
'strings' => [
'copied' => __('Copied!', 'wc-licensed-product'),
'copyFailed' => __('Copy failed', 'wc-licensed-product'),
'transferSuccess' => __('License transferred successfully!', 'wc-licensed-product'),
'transferError' => __('Transfer failed. Please try again.', 'wc-licensed-product'),
'transferConfirm' => __('Are you sure you want to transfer this license to a new domain? This action cannot be undone.', 'wc-licensed-product'),
'invalidDomain' => __('Please enter a valid domain.', 'wc-licensed-product'),
],
]);
}
/**
* Handle AJAX license transfer request from customer
*/
public function handleTransferRequest(): void
{
// Verify nonce
if (!check_ajax_referer('wclp_customer_transfer', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'wc-licensed-product')], 403);
}
// Verify user is logged in
$customerId = get_current_user_id();
if (!$customerId) {
wp_send_json_error(['message' => __('Please log in to transfer a license.', 'wc-licensed-product')], 401);
}
// Get and validate license ID
$licenseId = isset($_POST['license_id']) ? absint($_POST['license_id']) : 0;
if (!$licenseId) {
wp_send_json_error(['message' => __('Invalid license.', 'wc-licensed-product')], 400);
}
// Get and validate new domain
$newDomain = isset($_POST['new_domain']) ? sanitize_text_field($_POST['new_domain']) : '';
$newDomain = $this->normalizeDomain($newDomain);
if (empty($newDomain)) {
wp_send_json_error(['message' => __('Please enter a valid domain.', 'wc-licensed-product')], 400);
}
// Verify the license belongs to this customer
$license = $this->licenseManager->getLicenseById($licenseId);
if (!$license) {
wp_send_json_error(['message' => __('License not found.', 'wc-licensed-product')], 404);
}
if ($license->getCustomerId() !== $customerId) {
wp_send_json_error(['message' => __('You do not have permission to transfer this license.', 'wc-licensed-product')], 403);
}
// Check if license is in a transferable state
if ($license->getStatus() === 'revoked') {
wp_send_json_error(['message' => __('Revoked licenses cannot be transferred.', 'wc-licensed-product')], 400);
}
if ($license->getStatus() === 'expired') {
wp_send_json_error(['message' => __('Expired licenses cannot be transferred.', 'wc-licensed-product')], 400);
}
// Check if domain is the same
if ($license->getDomain() === $newDomain) {
wp_send_json_error(['message' => __('The new domain is the same as the current domain.', 'wc-licensed-product')], 400);
}
// Perform the transfer
$result = $this->licenseManager->transferLicense($licenseId, $newDomain);
if ($result) {
wp_send_json_success([
'message' => __('License transferred successfully!', 'wc-licensed-product'),
'new_domain' => $newDomain,
]);
} else {
wp_send_json_error(['message' => __('Failed to transfer license. Please try again.', 'wc-licensed-product')], 500);
}
}
/**
* Normalize domain for comparison and storage
*/
private function normalizeDomain(string $domain): string
{
// Remove protocol if present
$domain = preg_replace('#^https?://#i', '', $domain);
// Remove www prefix
$domain = preg_replace('#^www\.#i', '', $domain);
// Remove trailing slash
$domain = rtrim($domain, '/');
// Remove path if present
$domain = explode('/', $domain)[0];
// Convert to lowercase
$domain = strtolower($domain);
// Basic validation - must contain at least one dot and valid characters
if (!preg_match('/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/', $domain)) {
return '';
}
return $domain;
}
}