Files
wc-licensed-product/src/Admin/AdminController.php

1412 lines
62 KiB
PHP
Raw Normal View History

<?php
/**
* Admin Controller
*
* @package Jeremias\WcLicensedProduct\Admin
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Admin;
use Jeremias\WcLicensedProduct\License\License;
use Jeremias\WcLicensedProduct\License\LicenseManager;
use Twig\Environment;
/**
* Handles admin pages for license management
*/
final class AdminController
{
private Environment $twig;
private LicenseManager $licenseManager;
public function __construct(Environment $twig, LicenseManager $licenseManager)
{
$this->twig = $twig;
$this->licenseManager = $licenseManager;
$this->registerHooks();
}
/**
* Register WordPress hooks
*/
private function registerHooks(): void
{
// Add admin menu
add_action('admin_menu', [$this, 'addAdminMenu']);
// Enqueue admin styles
add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']);
// Handle admin actions
add_action('admin_init', [$this, 'handleAdminActions']);
// Add licenses column to orders list
add_filter('manage_edit-shop_order_columns', [$this, 'addOrdersLicenseColumn']);
add_action('manage_shop_order_posts_custom_column', [$this, 'displayOrdersLicenseColumn'], 10, 2);
// HPOS compatibility
add_filter('woocommerce_shop_order_list_table_columns', [$this, 'addOrdersLicenseColumn']);
add_action('woocommerce_shop_order_list_table_custom_column', [$this, 'displayOrdersLicenseColumnHpos'], 10, 2);
// Add to WooCommerce Reports
add_filter('woocommerce_admin_reports', [$this, 'addLicenseReports']);
// AJAX handler for live search
add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']);
}
/**
* Add admin menu pages
*/
public function addAdminMenu(): void
{
add_submenu_page(
'woocommerce',
__('Licenses', 'wc-licensed-product'),
__('Licenses', 'wc-licensed-product'),
'manage_woocommerce',
'wc-licenses',
[$this, 'renderLicensesPage']
);
}
/**
* Add license reports to WooCommerce Reports
*/
public function addLicenseReports(array $reports): array
{
$reports['licenses'] = [
'title' => __('Licenses', 'wc-licensed-product'),
'reports' => [
'overview' => [
'title' => __('Overview', 'wc-licensed-product'),
'description' => '',
'hide_title' => true,
'callback' => [$this, 'renderDashboardPage'],
],
],
];
return $reports;
}
/**
* Enqueue admin styles and scripts
*/
public function enqueueStyles(string $hook): void
{
// Check for our pages and WooCommerce Reports page with licenses tab
$isLicensePage = in_array($hook, ['woocommerce_page_wc-licenses', 'woocommerce_page_wc-license-dashboard'], true);
$isReportsPage = $hook === 'woocommerce_page_wc-reports' && isset($_GET['tab']) && $_GET['tab'] === 'licenses';
if (!$isLicensePage && !$isReportsPage) {
return;
}
wp_enqueue_style(
'wc-licensed-product-admin',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/css/admin.css',
[],
WC_LICENSED_PRODUCT_VERSION
);
// Enqueue live search script on licenses page
if ($isLicensePage) {
wp_enqueue_script(
'wc-licensed-product-admin',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/admin-licenses.js',
['jquery'],
WC_LICENSED_PRODUCT_VERSION,
true
);
wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wclp_live_search'),
'strings' => [
'noResults' => __('No licenses found', 'wc-licensed-product'),
'searching' => __('Searching...', 'wc-licensed-product'),
'error' => __('Search failed', 'wc-licensed-product'),
],
]);
}
}
/**
* Handle AJAX live search request
*/
public function handleLiveSearch(): void
{
check_ajax_referer('wclp_live_search', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
if (strlen($search) < 2) {
wp_send_json_success(['results' => []]);
}
$filters = ['search' => $search];
$licenses = $this->licenseManager->getAllLicenses(1, 10, $filters);
$results = [];
foreach ($licenses as $license) {
$product = wc_get_product($license->getProductId());
$customer = get_userdata($license->getCustomerId());
$results[] = [
'id' => $license->getId(),
'license_key' => $license->getLicenseKey(),
'domain' => $license->getDomain(),
'status' => $license->getStatus(),
'product_name' => $product ? $product->get_name() : __('Unknown', 'wc-licensed-product'),
'customer_name' => $customer ? $customer->display_name : __('Guest', 'wc-licensed-product'),
'customer_email' => $customer ? $customer->user_email : '',
'view_url' => admin_url('admin.php?page=wc-licenses&s=' . urlencode($license->getLicenseKey())),
];
}
wp_send_json_success(['results' => $results]);
}
/**
* Handle admin actions (update, delete licenses)
*/
public function handleAdminActions(): void
{
if (!isset($_GET['page']) || $_GET['page'] !== 'wc-licenses') {
return;
}
if (!current_user_can('manage_woocommerce')) {
return;
}
// Handle status update
if (isset($_POST['action']) && $_POST['action'] === 'update_license_status') {
$this->handleStatusUpdate();
}
// Handle delete
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['license_id'])) {
$this->handleDelete();
}
// Handle revoke
if (isset($_GET['action']) && $_GET['action'] === 'revoke' && isset($_GET['license_id'])) {
$this->handleRevoke();
}
// Handle extend
if (isset($_GET['action']) && $_GET['action'] === 'extend' && isset($_GET['license_id'])) {
$this->handleExtend();
}
// Handle set lifetime
if (isset($_GET['action']) && $_GET['action'] === 'lifetime' && isset($_GET['license_id'])) {
$this->handleSetLifetime();
}
// Handle bulk actions
if (isset($_POST['bulk_action']) && !empty($_POST['license_ids'])) {
$this->handleBulkAction();
}
// Handle transfer
if (isset($_POST['action']) && $_POST['action'] === 'transfer_license') {
$this->handleTransfer();
}
// Handle CSV export
if (isset($_GET['action']) && $_GET['action'] === 'export_csv') {
$this->handleCsvExport();
}
// Handle CSV import page
if (isset($_GET['action']) && $_GET['action'] === 'import_csv') {
// Show import form - handled in renderImportPage
}
// Handle CSV import upload
if (isset($_POST['action']) && $_POST['action'] === 'process_import_csv') {
$this->handleCsvImport();
}
}
/**
* Handle license status update
*/
private function handleStatusUpdate(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'update_license_status')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_POST['license_id'] ?? 0);
$status = sanitize_text_field($_POST['status'] ?? '');
if ($licenseId && in_array($status, [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_REVOKED], true)) {
$this->licenseManager->updateLicenseStatus($licenseId, $status);
wp_redirect(admin_url('admin.php?page=wc-licenses&updated=1'));
exit;
}
}
/**
* Handle license deletion
*/
private function handleDelete(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'delete_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
if ($licenseId) {
$this->licenseManager->deleteLicense($licenseId);
wp_redirect(admin_url('admin.php?page=wc-licenses&deleted=1'));
exit;
}
}
/**
* Handle license revocation
*/
private function handleRevoke(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'revoke_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
if ($licenseId) {
$this->licenseManager->updateLicenseStatus($licenseId, License::STATUS_REVOKED);
wp_redirect(admin_url('admin.php?page=wc-licenses&revoked=1'));
exit;
}
}
/**
* Handle license extension
*/
private function handleExtend(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'extend_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
$days = absint($_GET['days'] ?? 30);
if ($licenseId && $days > 0) {
$this->licenseManager->extendLicense($licenseId, $days);
wp_redirect(admin_url('admin.php?page=wc-licenses&extended=1'));
exit;
}
}
/**
* Handle set license to lifetime
*/
private function handleSetLifetime(): void
{
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'lifetime_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_GET['license_id'] ?? 0);
if ($licenseId) {
$this->licenseManager->setLicenseLifetime($licenseId);
wp_redirect(admin_url('admin.php?page=wc-licenses&lifetime=1'));
exit;
}
}
/**
* Handle license transfer
*/
private function handleTransfer(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'transfer_license')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$licenseId = absint($_POST['license_id'] ?? 0);
$newDomain = sanitize_text_field($_POST['new_domain'] ?? '');
if ($licenseId && !empty($newDomain)) {
$success = $this->licenseManager->transferLicense($licenseId, $newDomain);
if ($success) {
wp_redirect(admin_url('admin.php?page=wc-licenses&transferred=1'));
} else {
wp_redirect(admin_url('admin.php?page=wc-licenses&transfer_failed=1'));
}
exit;
}
wp_redirect(admin_url('admin.php?page=wc-licenses'));
exit;
}
/**
* Handle CSV export
*/
private function handleCsvExport(): void
{
if (!current_user_can('manage_woocommerce')) {
wp_die(__('You do not have permission to export licenses.', 'wc-licensed-product'));
}
$data = $this->licenseManager->exportLicensesForCsv();
if (empty($data)) {
wp_redirect(admin_url('admin.php?page=wc-licenses&export_empty=1'));
exit;
}
// Set headers for CSV download
$filename = 'licenses-export-' . gmdate('Y-m-d-His') . '.csv';
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
header('Pragma: no-cache');
header('Expires: 0');
$output = fopen('php://output', 'w');
// Write BOM for UTF-8
fwrite($output, "\xEF\xBB\xBF");
// Write header row
fputcsv($output, array_keys($data[0]));
// Write data rows
foreach ($data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;
}
/**
* Handle CSV import
*/
private function handleCsvImport(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'import_licenses_csv')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
if (!current_user_can('manage_woocommerce')) {
wp_die(__('You do not have permission to import licenses.', 'wc-licensed-product'));
}
// Check if file was uploaded
if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=upload'));
exit;
}
$file = $_FILES['import_file'];
// Validate file type
$fileType = wp_check_filetype($file['name']);
if ($fileType['ext'] !== 'csv') {
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=filetype'));
exit;
}
// Read the CSV file
$handle = fopen($file['tmp_name'], 'r');
if (!$handle) {
wp_redirect(admin_url('admin.php?page=wc-licenses&action=import_csv&import_error=read'));
exit;
}
// Get import options
$skipFirstRow = isset($_POST['skip_first_row']) && $_POST['skip_first_row'] === '1';
$updateExisting = isset($_POST['update_existing']) && $_POST['update_existing'] === '1';
// Skip BOM if present
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
// Read header row if skipping
if ($skipFirstRow) {
fgetcsv($handle);
}
$imported = 0;
$updated = 0;
$skipped = 0;
$errors = [];
while (($row = fgetcsv($handle)) !== false) {
// Skip empty rows
if (empty($row) || (count($row) === 1 && empty($row[0]))) {
continue;
}
// Map CSV columns (expected format from export):
// ID, License Key, Product, Product ID, Order ID, Order Number, Customer, Customer Email, Customer ID, Domain, Status, Activations, Max Activations, Expires At, Created At, Updated At
// For import we need: License Key (or generate), Product ID, Customer ID, Domain, Status, Max Activations, Expires At
$result = $this->processImportRow($row, $updateExisting);
if ($result === 'imported') {
$imported++;
} elseif ($result === 'updated') {
$updated++;
} elseif ($result === 'skipped') {
$skipped++;
} else {
$errors[] = $result;
}
}
fclose($handle);
// Build redirect URL with results
$redirectUrl = add_query_arg([
'page' => 'wc-licenses',
'imported' => $imported,
'updated' => $updated,
'skipped' => $skipped,
'import_errors' => count($errors),
], admin_url('admin.php'));
wp_redirect($redirectUrl);
exit;
}
/**
* Process a single import row
*
* @param array $row CSV row data
* @param bool $updateExisting Whether to update existing licenses
* @return string Result: 'imported', 'updated', 'skipped', or error message
*/
private function processImportRow(array $row, bool $updateExisting): string
{
// Determine if this is from our export format or simplified format
// Export format has 16 columns, simplified has fewer
if (count($row) >= 10) {
// Full export format
$licenseKey = trim($row[1] ?? '');
$productId = absint($row[3] ?? 0);
$orderId = absint($row[4] ?? 0);
$customerId = absint($row[8] ?? 0);
$domain = trim($row[9] ?? '');
$status = strtolower(trim($row[10] ?? 'active'));
$activationsCount = absint($row[11] ?? 1);
$maxActivations = absint($row[12] ?? 1);
$expiresAt = trim($row[13] ?? '');
} else {
// Simplified format: License Key, Product ID, Customer ID, Domain, Status, Max Activations, Expires At
$licenseKey = trim($row[0] ?? '');
$productId = absint($row[1] ?? 0);
$customerId = absint($row[2] ?? 0);
$domain = trim($row[3] ?? '');
$status = strtolower(trim($row[4] ?? 'active'));
$maxActivations = absint($row[5] ?? 1);
$expiresAt = trim($row[6] ?? '');
$orderId = 0;
$activationsCount = 1;
}
// Validate required fields
if (empty($domain)) {
return sprintf(__('Row missing domain', 'wc-licensed-product'));
}
if ($productId <= 0) {
return sprintf(__('Row missing valid product ID', 'wc-licensed-product'));
}
// Check if license key already exists
if (!empty($licenseKey)) {
$existing = $this->licenseManager->getLicenseByKey($licenseKey);
if ($existing) {
if ($updateExisting) {
// Update existing license
$this->licenseManager->updateLicenseDomain($existing->getId(), $domain);
if (in_array($status, [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_REVOKED], true)) {
$this->licenseManager->updateLicenseStatus($existing->getId(), $status);
}
return 'updated';
}
return 'skipped';
}
} else {
// Generate new license key
$licenseKey = $this->licenseManager->generateLicenseKey();
while ($this->licenseManager->getLicenseByKey($licenseKey)) {
$licenseKey = $this->licenseManager->generateLicenseKey();
}
}
// Normalize status
if (!in_array($status, [License::STATUS_ACTIVE, License::STATUS_INACTIVE, License::STATUS_EXPIRED, License::STATUS_REVOKED], true)) {
$status = License::STATUS_ACTIVE;
}
// Parse expiration date
$expiresAtParsed = null;
if (!empty($expiresAt) && strtolower($expiresAt) !== 'lifetime') {
try {
$expiresAtParsed = new \DateTimeImmutable($expiresAt);
} catch (\Exception $e) {
// Invalid date, leave as null (lifetime)
}
}
// Create the license
$result = $this->licenseManager->importLicense(
$licenseKey,
$productId,
$customerId,
$domain,
$orderId,
$status,
$maxActivations,
$activationsCount,
$expiresAtParsed
);
return $result ? 'imported' : sprintf(__('Failed to import license for domain %s', 'wc-licensed-product'), $domain);
}
/**
* Handle bulk actions
*/
private function handleBulkAction(): void
{
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'bulk_license_action')) {
wp_die(__('Security check failed.', 'wc-licensed-product'));
}
$action = sanitize_text_field($_POST['bulk_action'] ?? '');
$licenseIds = array_map('absint', (array) ($_POST['license_ids'] ?? []));
if (empty($licenseIds)) {
wp_redirect(admin_url('admin.php?page=wc-licenses'));
exit;
}
$count = 0;
switch ($action) {
case 'activate':
$count = $this->licenseManager->bulkUpdateStatus($licenseIds, License::STATUS_ACTIVE);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_activated=' . $count));
break;
case 'deactivate':
$count = $this->licenseManager->bulkUpdateStatus($licenseIds, License::STATUS_INACTIVE);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_deactivated=' . $count));
break;
case 'revoke':
$count = $this->licenseManager->bulkUpdateStatus($licenseIds, License::STATUS_REVOKED);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_revoked=' . $count));
break;
case 'delete':
$count = $this->licenseManager->bulkDelete($licenseIds);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_deleted=' . $count));
break;
case 'extend_30':
$count = $this->licenseManager->bulkExtend($licenseIds, 30);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_extended=' . $count));
break;
case 'extend_90':
$count = $this->licenseManager->bulkExtend($licenseIds, 90);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_extended=' . $count));
break;
case 'extend_365':
$count = $this->licenseManager->bulkExtend($licenseIds, 365);
wp_redirect(admin_url('admin.php?page=wc-licenses&bulk_extended=' . $count));
break;
default:
wp_redirect(admin_url('admin.php?page=wc-licenses'));
}
exit;
}
/**
* Render license dashboard page
*/
public function renderDashboardPage(): void
{
$stats = $this->licenseManager->getStatistics();
try {
echo $this->twig->render('admin/dashboard.html.twig', [
'stats' => $stats,
'admin_url' => admin_url('admin.php'),
]);
} catch (\Exception $e) {
// Fallback to PHP template
$this->renderDashboardPageFallback($stats);
}
}
/**
* Fallback render for dashboard page
*/
private function renderDashboardPageFallback(array $stats): void
{
?>
<div class="wrap wclp-dashboard">
<h1><?php esc_html_e('License Dashboard', 'wc-licensed-product'); ?></h1>
<div class="wclp-dashboard-stats">
<div class="wclp-stat-cards">
<div class="wclp-stat-card wclp-stat-total">
<div class="wclp-stat-icon"><span class="dashicons dashicons-admin-network"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['total']); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Total Licenses', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-active">
<div class="wclp-stat-icon"><span class="dashicons dashicons-yes-alt"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_ACTIVE]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Active', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-inactive">
<div class="wclp-stat-icon"><span class="dashicons dashicons-marker"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_INACTIVE]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-expired">
<div class="wclp-stat-icon"><span class="dashicons dashicons-calendar-alt"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_EXPIRED]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Expired', 'wc-licensed-product'); ?></span>
</div>
</div>
<div class="wclp-stat-card wclp-stat-revoked">
<div class="wclp-stat-icon"><span class="dashicons dashicons-dismiss"></span></div>
<div class="wclp-stat-content">
<span class="wclp-stat-number"><?php echo esc_html($stats['by_status'][License::STATUS_REVOKED]); ?></span>
<span class="wclp-stat-label"><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></span>
</div>
</div>
</div>
<?php if ($stats['expiring_soon'] > 0): ?>
<div class="notice notice-warning">
<p>
<span class="dashicons dashicons-warning"></span>
<strong><?php esc_html_e('Attention:', 'wc-licensed-product'); ?></strong>
<?php
printf(
/* translators: %d: number of licenses expiring */
_n(
'%d license is expiring within the next 30 days.',
'%d licenses are expiring within the next 30 days.',
$stats['expiring_soon'],
'wc-licensed-product'
),
$stats['expiring_soon']
);
?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>"><?php esc_html_e('View Licenses', 'wc-licensed-product'); ?></a>
</p>
</div>
<?php endif; ?>
<div class="wclp-dashboard-actions">
<h2><?php esc_html_e('Quick Actions', 'wc-licensed-product'); ?></h2>
<div class="wclp-action-buttons">
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="button button-primary">
<span class="dashicons dashicons-admin-network"></span>
<?php esc_html_e('Manage Licenses', 'wc-licensed-product'); ?>
</a>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="button">
<span class="dashicons dashicons-download"></span>
<?php esc_html_e('Export to CSV', 'wc-licensed-product'); ?>
</a>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-settings&tab=licensed_product')); ?>" class="button">
<span class="dashicons dashicons-admin-generic"></span>
<?php esc_html_e('Settings', 'wc-licensed-product'); ?>
</a>
</div>
</div>
</div>
</div>
<?php
}
/**
* Render licenses admin page
*/
public function renderLicensesPage(): void
{
// Check if showing import page
if (isset($_GET['action']) && $_GET['action'] === 'import_csv') {
$this->renderImportPage();
return;
}
$page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
$perPage = 20;
// Build filters from query params
$filters = [];
if (!empty($_GET['s'])) {
$filters['search'] = sanitize_text_field($_GET['s']);
}
if (!empty($_GET['status']) && $_GET['status'] !== 'all') {
$filters['status'] = sanitize_text_field($_GET['status']);
}
if (!empty($_GET['product_id'])) {
$filters['product_id'] = absint($_GET['product_id']);
}
$licenses = $this->licenseManager->getAllLicenses($page, $perPage, $filters);
$totalLicenses = $this->licenseManager->getLicenseCount($filters);
$totalPages = (int) ceil($totalLicenses / $perPage);
// Get products for filter dropdown
$licensedProducts = $this->licenseManager->getLicensedProducts();
// Enrich licenses with related data
$enrichedLicenses = [];
foreach ($licenses as $license) {
$product = wc_get_product($license->getProductId());
$order = wc_get_order($license->getOrderId());
$customer = get_userdata($license->getCustomerId());
$enrichedLicenses[] = [
'license' => $license,
'product_name' => $product ? $product->get_name() : __('Unknown', 'wc-licensed-product'),
'product_edit_url' => $product ? get_edit_post_link($product->get_id()) : '',
'order_number' => $order ? $order->get_order_number() : '',
'order_edit_url' => $order ? $order->get_edit_order_url() : '',
'customer_name' => $customer ? $customer->display_name : __('Guest', 'wc-licensed-product'),
'customer_email' => $customer ? $customer->user_email : '',
];
}
// Add URL helper functions to Twig
$this->twig->addFunction(new \Twig\TwigFunction('extend_url', function (int $licenseId, int $days = 30): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $licenseId . '&days=' . $days),
'extend_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('lifetime_url', function (int $licenseId): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=lifetime&license_id=' . $licenseId),
'lifetime_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('revoke_url', function (int $licenseId): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=revoke&license_id=' . $licenseId),
'revoke_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('delete_url', function (int $licenseId): string {
return wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=delete&license_id=' . $licenseId),
'delete_license'
);
}));
$this->twig->addFunction(new \Twig\TwigFunction('transfer_nonce', function (): string {
return wp_create_nonce('transfer_license');
}));
try {
echo $this->twig->render('admin/licenses.html.twig', [
'licenses' => $enrichedLicenses,
'current_page' => $page,
'total_pages' => $totalPages,
'total_licenses' => $totalLicenses,
'admin_url' => admin_url('admin.php?page=wc-licenses'),
'notices' => $this->getNotices(),
'filters' => $filters,
'products' => $licensedProducts,
]);
} catch (\Exception $e) {
// Fallback to PHP template
$this->renderLicensesPageFallback($enrichedLicenses, $page, $totalPages, $totalLicenses, $filters, $licensedProducts);
}
}
/**
* Get admin notices
*/
private function getNotices(): array
{
$notices = [];
if (isset($_GET['updated'])) {
$notices[] = ['type' => 'success', 'message' => __('License updated successfully.', 'wc-licensed-product')];
}
if (isset($_GET['deleted'])) {
$notices[] = ['type' => 'success', 'message' => __('License deleted successfully.', 'wc-licensed-product')];
}
if (isset($_GET['revoked'])) {
$notices[] = ['type' => 'success', 'message' => __('License revoked successfully.', 'wc-licensed-product')];
}
if (isset($_GET['extended'])) {
$notices[] = ['type' => 'success', 'message' => __('License extended successfully.', 'wc-licensed-product')];
}
if (isset($_GET['lifetime'])) {
$notices[] = ['type' => 'success', 'message' => __('License set to lifetime successfully.', 'wc-licensed-product')];
}
if (isset($_GET['bulk_activated'])) {
$count = absint($_GET['bulk_activated']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license activated.', '%d licenses activated.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_deactivated'])) {
$count = absint($_GET['bulk_deactivated']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license deactivated.', '%d licenses deactivated.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_revoked'])) {
$count = absint($_GET['bulk_revoked']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license revoked.', '%d licenses revoked.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_deleted'])) {
$count = absint($_GET['bulk_deleted']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license deleted.', '%d licenses deleted.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['bulk_extended'])) {
$count = absint($_GET['bulk_extended']);
$notices[] = ['type' => 'success', 'message' => sprintf(
/* translators: %d: number of licenses */
_n('%d license extended.', '%d licenses extended.', $count, 'wc-licensed-product'),
$count
)];
}
if (isset($_GET['transferred'])) {
$notices[] = ['type' => 'success', 'message' => __('License transferred to new domain successfully.', 'wc-licensed-product')];
}
if (isset($_GET['transfer_failed'])) {
$notices[] = ['type' => 'error', 'message' => __('Failed to transfer license. The license may be revoked or invalid.', 'wc-licensed-product')];
}
if (isset($_GET['export_empty'])) {
$notices[] = ['type' => 'warning', 'message' => __('No licenses to export.', 'wc-licensed-product')];
}
if (isset($_GET['imported'])) {
$imported = absint($_GET['imported']);
$updated = absint($_GET['updated'] ?? 0);
$skipped = absint($_GET['skipped'] ?? 0);
$errors = absint($_GET['import_errors'] ?? 0);
$message = sprintf(
/* translators: %d: number of licenses imported */
_n('%d license imported.', '%d licenses imported.', $imported, 'wc-licensed-product'),
$imported
);
if ($updated > 0) {
$message .= ' ' . sprintf(
/* translators: %d: number of licenses updated */
_n('%d updated.', '%d updated.', $updated, 'wc-licensed-product'),
$updated
);
}
if ($skipped > 0) {
$message .= ' ' . sprintf(
/* translators: %d: number of licenses skipped */
_n('%d skipped.', '%d skipped.', $skipped, 'wc-licensed-product'),
$skipped
);
}
if ($errors > 0) {
$message .= ' ' . sprintf(
/* translators: %d: number of errors */
_n('%d error.', '%d errors.', $errors, 'wc-licensed-product'),
$errors
);
}
$notices[] = ['type' => 'success', 'message' => $message];
}
return $notices;
}
/**
* Fallback render for licenses page
*/
private function renderLicensesPageFallback(array $enrichedLicenses, int $page, int $totalPages, int $totalLicenses, array $filters = [], array $products = []): void
{
?>
<div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Licenses', 'wc-licensed-product'); ?></h1>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=export_csv')); ?>" class="page-title-action">
<span class="dashicons dashicons-download" style="vertical-align: middle;"></span>
<?php esc_html_e('Export CSV', 'wc-licensed-product'); ?>
</a>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses&action=import_csv')); ?>" class="page-title-action">
<span class="dashicons dashicons-upload" style="vertical-align: middle;"></span>
<?php esc_html_e('Import CSV', 'wc-licensed-product'); ?>
</a>
<hr class="wp-header-end">
<?php foreach ($this->getNotices() as $notice): ?>
<div class="notice notice-<?php echo esc_attr($notice['type']); ?> is-dismissible">
<p><?php echo esc_html($notice['message']); ?></p>
</div>
<?php endforeach; ?>
<!-- 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"><?php esc_html_e('Search Licenses', 'wc-licensed-product'); ?></label>
<input type="search" id="license-search-input" name="s" value="<?php echo esc_attr($filters['search'] ?? ''); ?>"
placeholder="<?php esc_attr_e('Search license key or domain...', 'wc-licensed-product'); ?>">
<input type="submit" id="search-submit" class="button" value="<?php esc_attr_e('Search', 'wc-licensed-product'); ?>">
</p>
<div class="tablenav top">
<div class="alignleft actions">
<select name="status">
<option value="all"><?php esc_html_e('All Statuses', 'wc-licensed-product'); ?></option>
<option value="active" <?php selected($filters['status'] ?? '', 'active'); ?>><?php esc_html_e('Active', 'wc-licensed-product'); ?></option>
<option value="inactive" <?php selected($filters['status'] ?? '', 'inactive'); ?>><?php esc_html_e('Inactive', 'wc-licensed-product'); ?></option>
<option value="expired" <?php selected($filters['status'] ?? '', 'expired'); ?>><?php esc_html_e('Expired', 'wc-licensed-product'); ?></option>
<option value="revoked" <?php selected($filters['status'] ?? '', 'revoked'); ?>><?php esc_html_e('Revoked', 'wc-licensed-product'); ?></option>
</select>
<select name="product_id">
<option value=""><?php esc_html_e('All Products', 'wc-licensed-product'); ?></option>
<?php foreach ($products as $id => $name): ?>
<option value="<?php echo esc_attr($id); ?>" <?php selected($filters['product_id'] ?? '', $id); ?>><?php echo esc_html($name); ?></option>
<?php endforeach; ?>
</select>
<input type="submit" class="button" value="<?php esc_attr_e('Filter', 'wc-licensed-product'); ?>">
<?php if (!empty($filters)): ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="button"><?php esc_html_e('Clear', 'wc-licensed-product'); ?></a>
<?php endif; ?>
</div>
<div class="tablenav-pages">
<span class="displaying-num"><?php echo esc_html($totalLicenses); ?> <?php echo $totalLicenses === 1 ? esc_html__('item', 'wc-licensed-product') : esc_html__('items', 'wc-licensed-product'); ?></span>
</div>
</div>
</form>
<p class="description">
<?php esc_html_e('Showing', 'wc-licensed-product'); ?> <?php echo esc_html($totalLicenses); ?> <?php echo $totalLicenses === 1 ? esc_html__('license', 'wc-licensed-product') : esc_html__('licenses', 'wc-licensed-product'); ?>
<?php if (!empty($filters)): ?>
(<?php esc_html_e('filtered', 'wc-licensed-product'); ?>)
<?php endif; ?>
| <a href="<?php echo esc_url(admin_url('admin.php?page=wc-reports&tab=licenses')); ?>"><?php esc_html_e('View Dashboard', 'wc-licensed-product'); ?></a>
</p>
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>">
<?php wp_nonce_field('bulk_license_action'); ?>
<div class="tablenav top">
<div class="alignleft actions bulkactions">
<select name="bulk_action" id="bulk-action-selector">
<option value=""><?php esc_html_e('Bulk Actions', 'wc-licensed-product'); ?></option>
<option value="activate"><?php esc_html_e('Activate', 'wc-licensed-product'); ?></option>
<option value="deactivate"><?php esc_html_e('Deactivate', 'wc-licensed-product'); ?></option>
<option value="revoke"><?php esc_html_e('Revoke', 'wc-licensed-product'); ?></option>
<option value="extend_30"><?php esc_html_e('Extend 30 days', 'wc-licensed-product'); ?></option>
<option value="extend_90"><?php esc_html_e('Extend 90 days', 'wc-licensed-product'); ?></option>
<option value="extend_365"><?php esc_html_e('Extend 1 year', 'wc-licensed-product'); ?></option>
<option value="delete"><?php esc_html_e('Delete', 'wc-licensed-product'); ?></option>
</select>
<input type="submit" class="button action" value="<?php esc_attr_e('Apply', 'wc-licensed-product'); ?>">
</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><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Product', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($enrichedLicenses)): ?>
<tr>
<td colspan="8"><?php esc_html_e('No licenses found.', 'wc-licensed-product'); ?></td>
</tr>
<?php else: ?>
<?php foreach ($enrichedLicenses as $item): ?>
<tr>
<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>
<?php if ($item['product_edit_url']): ?>
<a href="<?php echo esc_url($item['product_edit_url']); ?>">
<?php echo esc_html($item['product_name']); ?>
</a>
<?php else: ?>
<?php echo esc_html($item['product_name']); ?>
<?php endif; ?>
</td>
<td>
<?php echo esc_html($item['customer_name']); ?>
<?php if ($item['customer_email']): ?>
<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>
<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>
<td class="license-actions">
<div class="row-actions">
<?php if ($item['license']->getStatus() !== License::STATUS_REVOKED): ?>
<span class="extend">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=extend&license_id=' . $item['license']->getId() . '&days=30'),
'extend_license'
)); ?>" title="<?php esc_attr_e('Extend by 30 days', 'wc-licensed-product'); ?>">+30d</a> |
</span>
<span class="lifetime">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=lifetime&license_id=' . $item['license']->getId()),
'lifetime_license'
)); ?>" title="<?php esc_attr_e('Set to lifetime', 'wc-licensed-product'); ?>">∞</a> |
</span>
<span class="revoke">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=revoke&license_id=' . $item['license']->getId()),
'revoke_license'
)); ?>" onclick="return confirm('<?php esc_attr_e('Are you sure?', 'wc-licensed-product'); ?>')">
<?php esc_html_e('Revoke', 'wc-licensed-product'); ?>
</a> |
</span>
<?php endif; ?>
<span class="delete">
<a href="<?php echo esc_url(wp_nonce_url(
admin_url('admin.php?page=wc-licenses&action=delete&license_id=' . $item['license']->getId()),
'delete_license'
)); ?>" class="submitdelete" onclick="return confirm('<?php esc_attr_e('Are you sure you want to delete this license?', 'wc-licensed-product'); ?>')">
<?php esc_html_e('Delete', 'wc-licensed-product'); ?>
</a>
</span>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<tfoot>
<tr>
<td class="manage-column column-cb check-column">
<input type="checkbox" id="cb-select-all-2">
</td>
<th><?php esc_html_e('License Key', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Product', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Customer', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Domain', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Status', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Expires', 'wc-licensed-product'); ?></th>
<th><?php esc_html_e('Actions', 'wc-licensed-product'); ?></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=""><?php esc_html_e('Bulk Actions', 'wc-licensed-product'); ?></option>
<option value="activate"><?php esc_html_e('Activate', 'wc-licensed-product'); ?></option>
<option value="deactivate"><?php esc_html_e('Deactivate', 'wc-licensed-product'); ?></option>
<option value="revoke"><?php esc_html_e('Revoke', 'wc-licensed-product'); ?></option>
<option value="extend_30"><?php esc_html_e('Extend 30 days', 'wc-licensed-product'); ?></option>
<option value="extend_90"><?php esc_html_e('Extend 90 days', 'wc-licensed-product'); ?></option>
<option value="extend_365"><?php esc_html_e('Extend 1 year', 'wc-licensed-product'); ?></option>
<option value="delete"><?php esc_html_e('Delete', 'wc-licensed-product'); ?></option>
</select>
<input type="submit" class="button action" value="<?php esc_attr_e('Apply', 'wc-licensed-product'); ?>">
</div>
<?php if ($totalPages > 1): ?>
<div class="tablenav-pages">
<?php
echo paginate_links([
'base' => admin_url('admin.php?page=wc-licenses&paged=%#%'),
'format' => '',
'current' => $page,
'total' => $totalPages,
]);
?>
</div>
<?php endif; ?>
</div>
</form>
<script>
(function($) {
$('#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);
});
$('#bulk-action-selector, #bulk-action-selector-bottom').on('change', function() {
$('#bulk-action-selector, #bulk-action-selector-bottom').val($(this).val());
});
$('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);
}
});
})(jQuery);
</script>
</div>
<?php
}
/**
* Render the CSV import page
*/
private function renderImportPage(): void
{
$importError = $_GET['import_error'] ?? '';
?>
<div class="wrap">
<h1>
<?php esc_html_e('Import Licenses', 'wc-licensed-product'); ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" class="page-title-action">
<?php esc_html_e('Back to Licenses', 'wc-licensed-product'); ?>
</a>
</h1>
<?php if ($importError): ?>
<div class="notice notice-error">
<p>
<?php
switch ($importError) {
case 'upload':
esc_html_e('Error uploading file. Please try again.', 'wc-licensed-product');
break;
case 'filetype':
esc_html_e('Invalid file type. Please upload a CSV file.', 'wc-licensed-product');
break;
case 'read':
esc_html_e('Error reading file. Please check the file format.', 'wc-licensed-product');
break;
default:
esc_html_e('An error occurred during import.', 'wc-licensed-product');
}
?>
</p>
</div>
<?php endif; ?>
<div class="card" style="max-width: 800px; padding: 20px;">
<h2><?php esc_html_e('Import Licenses from CSV', 'wc-licensed-product'); ?></h2>
<p class="description">
<?php esc_html_e('Upload a CSV file to import licenses. You can use the exported CSV format or a simplified format.', 'wc-licensed-product'); ?>
</p>
<h3><?php esc_html_e('CSV Format', 'wc-licensed-product'); ?></h3>
<p class="description">
<?php esc_html_e('The CSV file should contain the following columns:', 'wc-licensed-product'); ?>
</p>
<div style="background: #f8f9fa; padding: 15px; border-radius: 4px; margin: 15px 0;">
<p><strong><?php esc_html_e('Full Format (from Export):', 'wc-licensed-product'); ?></strong></p>
<code style="display: block; margin-bottom: 10px; font-size: 12px;">ID, License Key, Product, Product ID, Order ID, Order Number, Customer, Customer Email, Customer ID, Domain, Status, Activations, Max Activations, Expires At, Created At, Updated At</code>
<p><strong><?php esc_html_e('Simplified Format:', 'wc-licensed-product'); ?></strong></p>
<code style="display: block; font-size: 12px;">License Key, Product ID, Customer ID, Domain, Status, Max Activations, Expires At</code>
</div>
<p class="description">
<strong><?php esc_html_e('Notes:', 'wc-licensed-product'); ?></strong><br>
- <?php esc_html_e('Leave License Key empty to auto-generate.', 'wc-licensed-product'); ?><br>
- <?php esc_html_e('Status can be: active, inactive, expired, revoked (defaults to active).', 'wc-licensed-product'); ?><br>
- <?php esc_html_e('Expires At should be in YYYY-MM-DD format or "Lifetime".', 'wc-licensed-product'); ?>
</p>
<hr style="margin: 20px 0;">
<form method="post" action="<?php echo esc_url(admin_url('admin.php?page=wc-licenses')); ?>" enctype="multipart/form-data">
<?php wp_nonce_field('import_licenses_csv'); ?>
<input type="hidden" name="action" value="process_import_csv">
<table class="form-table">
<tr>
<th scope="row">
<label for="import_file"><?php esc_html_e('CSV File', 'wc-licensed-product'); ?></label>
</th>
<td>
<input type="file" name="import_file" id="import_file" accept=".csv" required>
<p class="description"><?php esc_html_e('Select a CSV file to import.', 'wc-licensed-product'); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e('Options', 'wc-licensed-product'); ?></th>
<td>
<label>
<input type="checkbox" name="skip_first_row" value="1" checked>
<?php esc_html_e('Skip first row (header row)', 'wc-licensed-product'); ?>
</label>
<br><br>
<label>
<input type="checkbox" name="update_existing" value="1">
<?php esc_html_e('Update existing licenses (by license key)', 'wc-licensed-product'); ?>
</label>
<p class="description">
<?php esc_html_e('If enabled, licenses with matching keys will be updated instead of skipped.', 'wc-licensed-product'); ?>
</p>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary">
<span class="dashicons dashicons-upload" style="vertical-align: middle;"></span>
<?php esc_html_e('Import Licenses', 'wc-licensed-product'); ?>
</button>
</p>
</form>
</div>
</div>
<?php
}
/**
* Add license column to orders list
*/
public function addOrdersLicenseColumn(array $columns): array
{
$newColumns = [];
foreach ($columns as $key => $value) {
$newColumns[$key] = $value;
if ($key === 'order_status') {
$newColumns['license'] = __('License', 'wc-licensed-product');
}
}
return $newColumns;
}
/**
* Display license column content
*/
public function displayOrdersLicenseColumn(string $column, int $postId): void
{
if ($column !== 'license') {
return;
}
$order = wc_get_order($postId);
$this->outputLicenseColumnContent($order);
}
/**
* Display license column content (HPOS)
*/
public function displayOrdersLicenseColumnHpos(string $column, \WC_Order $order): void
{
if ($column !== 'license') {
return;
}
$this->outputLicenseColumnContent($order);
}
/**
* Output license column content
*/
private function outputLicenseColumnContent(?\WC_Order $order): void
{
if (!$order) {
echo '—';
return;
}
$hasLicensedProduct = false;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && $product->is_type('licensed')) {
$hasLicensedProduct = true;
break;
}
}
if (!$hasLicensedProduct) {
echo '—';
return;
}
$domain = $order->get_meta('_licensed_product_domain');
if ($domain) {
echo '<span class="dashicons dashicons-admin-network"></span> ' . esc_html($domain);
} else {
echo '<span class="dashicons dashicons-warning" title="' . esc_attr__('No domain specified', 'wc-licensed-product') . '"></span>';
}
}
}