twig = $twig; $this->licenseManager = $licenseManager; $this->versionManager = $versionManager; $this->downloadController = $downloadController; $this->registerHooks(); } /** * Register WordPress hooks */ private function registerHooks(): void { // Add licenses endpoint add_action('init', [$this, 'addLicensesEndpoint']); // Register endpoint query var with WooCommerce add_filter('woocommerce_get_query_vars', [$this, 'addLicensesQueryVar']); // Add licenses menu item add_filter('woocommerce_account_menu_items', [$this, 'addLicensesMenuItem']); // Add licenses endpoint content add_action('woocommerce_account_licenses_endpoint', [$this, 'displayLicensesContent']); // 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']); } /** * Add licenses endpoint for My Account */ public function addLicensesEndpoint(): void { add_rewrite_endpoint('licenses', EP_ROOT | EP_PAGES); } /** * Register licenses query var with WooCommerce */ public function addLicensesQueryVar(array $vars): array { $vars['licenses'] = 'licenses'; return $vars; } /** * Add licenses menu item to My Account navigation */ public function addLicensesMenuItem(array $items): array { // Insert licenses after orders $newItems = []; foreach ($items as $key => $value) { $newItems[$key] = $value; if ($key === 'orders') { $newItems['licenses'] = __('Licenses', 'wc-licensed-product'); } } return $newItems; } /** * Display licenses content in My Account */ public function displayLicensesContent(): void { $customerId = get_current_user_id(); if (!$customerId) { echo '

' . esc_html__('Please log in to view your licenses.', 'wc-licensed-product') . '

'; return; } $licenses = $this->licenseManager->getLicensesByCustomer($customerId); // Group licenses by product+order into "packages" $packages = $this->groupLicensesIntoPackages($licenses); try { echo $this->twig->render('frontend/licenses.html.twig', [ 'packages' => $packages, 'has_packages' => !empty($packages), 'signing_enabled' => ResponseSigner::isSigningEnabled(), ]); } catch (\Exception $e) { // Fallback to PHP template if Twig fails $this->displayLicensesFallback($packages); } } /** * Group licenses into packages by product+order * * @param array $licenses Array of License objects * @return array Array of package data */ private function groupLicensesIntoPackages(array $licenses): array { $grouped = []; foreach ($licenses as $license) { $productId = $license->getProductId(); $orderId = $license->getOrderId(); $key = $productId . '_' . $orderId; if (!isset($grouped[$key])) { $product = wc_get_product($productId); $order = wc_get_order($orderId); $grouped[$key] = [ 'product_id' => $productId, 'order_id' => $orderId, 'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'), 'product_url' => $product ? $product->get_permalink() : '', 'order_number' => $order ? $order->get_order_number() : '', 'order_url' => $order ? $order->get_view_order_url() : '', 'licenses' => [], 'downloads' => [], 'has_active_license' => false, ]; } // Add license to package $grouped[$key]['licenses'][] = [ 'id' => $license->getId(), 'license_key' => $license->getLicenseKey(), 'domain' => $license->getDomain(), 'status' => $license->getStatus(), 'expires_at' => $license->getExpiresAt(), 'is_transferable' => in_array($license->getStatus(), ['active', 'inactive'], true), 'customer_secret' => ResponseSigner::getCustomerSecretForLicense($license->getLicenseKey()), ]; // Track if package has at least one active license if ($license->getStatus() === 'active') { $grouped[$key]['has_active_license'] = true; } } // Add downloads for packages with active licenses foreach ($grouped as $key => &$package) { if ($package['has_active_license']) { $package['downloads'] = $this->getDownloadsForProduct( $package['product_id'], $package['licenses'][0]['id'] // Use first license for download URL ); } } // Sort by order date (newest first) - re-index array return array_values($grouped); } /** * Get downloads for a product */ private function getDownloadsForProduct(int $productId, int $licenseId): array { $downloads = []; $versions = $this->versionManager->getVersionsByProduct($productId); foreach ($versions as $version) { if ($version->isActive() && ($version->getAttachmentId() || $version->getDownloadUrl())) { $downloads[] = [ 'version' => $version->getVersion(), 'version_id' => $version->getId(), 'filename' => $version->getDownloadFilename(), 'download_url' => $this->downloadController->generateDownloadUrl( $licenseId, $version->getId() ), 'release_notes' => $version->getReleaseNotes(), 'released_at' => $version->getReleasedAt()->format(get_option('date_format')), 'file_hash' => $version->getFileHash(), ]; } } return $downloads; } /** * Fallback display method if Twig is unavailable */ private function displayLicensesFallback(array $packages): void { if (empty($packages)) { echo '

' . esc_html__('You have no licenses yet.', 'wc-licensed-product') . '

'; return; } ?>
format('Y-m-d')) : '' . esc_html__('Lifetime', 'wc-licensed-product') . ''; ?>

  • ...
1): ?>
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; } }