Implement self-licensing (v0.3.0) and settings sub-tabs (v0.3.1)

v0.3.0 - Self-Licensing:
- Add PluginLicenseChecker singleton for license validation
- Integrate magdev/wc-licensed-product-client library
- Add license settings: server URL, key, optional secret
- Disable frontend features without valid license (except localhost)
- Add license status display with verify button in settings

v0.3.1 - Settings UI Improvements:
- Reorganize settings page with WooCommerce-style sub-tabs
- Split settings into: Plugin License, Default Settings, Notifications
- Use PHP 8 match expression for section-specific rendering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 18:32:17 +01:00
parent 6fe3a88592
commit e9763192f6
10 changed files with 2648 additions and 1165 deletions

View File

@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Admin;
use Jeremias\WcLicensedProduct\License\PluginLicenseChecker;
/**
* Handles WooCommerce settings tab for license defaults
*/
@@ -19,6 +21,11 @@ final class SettingsController
*/
public const OPTION_NAME = 'wc_licensed_product_settings';
/**
* Tab ID
*/
private const TAB_ID = 'licensed_product';
/**
* Constructor
*/
@@ -33,8 +40,10 @@ final class SettingsController
private function registerHooks(): void
{
add_filter('woocommerce_settings_tabs_array', [$this, 'addSettingsTab'], 50);
add_action('woocommerce_settings_tabs_licensed_product', [$this, 'renderSettingsTab']);
add_action('woocommerce_update_options_licensed_product', [$this, 'saveSettings']);
add_action('woocommerce_sections_' . self::TAB_ID, [$this, 'outputSections']);
add_action('woocommerce_settings_' . self::TAB_ID, [$this, 'renderSettingsTab']);
add_action('woocommerce_update_options_' . self::TAB_ID, [$this, 'saveSettings']);
add_action('wp_ajax_wclp_verify_plugin_license', [$this, 'handleVerifyLicense']);
}
/**
@@ -42,14 +51,119 @@ final class SettingsController
*/
public function addSettingsTab(array $tabs): array
{
$tabs['licensed_product'] = __('Licensed Products', 'wc-licensed-product');
$tabs[self::TAB_ID] = __('Licensed Products', 'wc-licensed-product');
return $tabs;
}
/**
* Get settings fields
* Get available sections
*/
public function getSections(): array
{
return [
'' => __('Plugin License', 'wc-licensed-product'),
'defaults' => __('Default Settings', 'wc-licensed-product'),
'notifications' => __('Notifications', 'wc-licensed-product'),
];
}
/**
* Get current section from URL
*/
private function getCurrentSection(): string
{
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset($_GET['section']) ? sanitize_title(wp_unslash($_GET['section'])) : '';
}
/**
* Output sections navigation (sub-tabs)
*/
public function outputSections(): void
{
$sections = $this->getSections();
if (empty($sections) || count($sections) <= 1) {
return;
}
$currentSection = $this->getCurrentSection();
echo '<ul class="subsubsub">';
$arrayKeys = array_keys($sections);
foreach ($sections as $id => $label) {
$url = admin_url('admin.php?page=wc-settings&tab=' . self::TAB_ID . '&section=' . sanitize_title($id));
$class = ($currentSection === $id) ? 'current' : '';
$separator = (end($arrayKeys) === $id) ? '' : ' | ';
echo '<li><a href="' . esc_url($url) . '" class="' . esc_attr($class) . '">' . esc_html($label) . '</a>' . $separator . '</li>';
}
echo '</ul><br class="clear" />';
}
/**
* Get settings fields for the current section
*/
public function getSettingsFields(): array
{
$currentSection = $this->getCurrentSection();
return match ($currentSection) {
'defaults' => $this->getDefaultsSettings(),
'notifications' => $this->getNotificationsSettings(),
default => $this->getPluginLicenseSettings(),
};
}
/**
* Get plugin license settings (default section)
*/
private function getPluginLicenseSettings(): array
{
return [
'plugin_license_section_title' => [
'name' => __('Plugin License', 'wc-licensed-product'),
'type' => 'title',
'desc' => __('Configure the license for this plugin. A valid license is required for frontend features to work.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_section_plugin_license',
],
'plugin_license_server_url' => [
'name' => __('License Server URL', 'wc-licensed-product'),
'type' => 'url',
'desc' => __('The URL of the license server (e.g., https://shop.example.com).', 'wc-licensed-product'),
'id' => 'wc_licensed_product_plugin_license_server_url',
'default' => '',
'placeholder' => 'https://shop.example.com',
],
'plugin_license_key' => [
'name' => __('License Key', 'wc-licensed-product'),
'type' => 'text',
'desc' => __('Your license key in XXXX-XXXX-XXXX-XXXX format.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_plugin_license_key',
'default' => '',
'placeholder' => 'XXXX-XXXX-XXXX-XXXX',
],
'plugin_license_server_secret' => [
'name' => __('Server Secret (Optional)', 'wc-licensed-product'),
'type' => 'password',
'desc' => __('If the license server uses signed responses, enter the shared secret here for enhanced security.', 'wc-licensed-product'),
'id' => 'wc_licensed_product_plugin_license_server_secret',
'default' => '',
],
'plugin_license_section_end' => [
'type' => 'sectionend',
'id' => 'wc_licensed_product_section_plugin_license_end',
],
];
}
/**
* Get default license settings
*/
private function getDefaultsSettings(): array
{
return [
'section_title' => [
@@ -92,7 +206,15 @@ final class SettingsController
'type' => 'sectionend',
'id' => 'wc_licensed_product_section_defaults_end',
],
// Email settings section
];
}
/**
* Get notifications settings
*/
private function getNotificationsSettings(): array
{
return [
'email_section_title' => [
'name' => __('Expiration Warning Schedule', 'wc-licensed-product'),
'type' => 'title',
@@ -138,9 +260,96 @@ final class SettingsController
*/
public function renderSettingsTab(): void
{
$currentSection = $this->getCurrentSection();
// Only show license status on the plugin license section
if ($currentSection === '') {
$this->renderLicenseStatus();
}
woocommerce_admin_fields($this->getSettingsFields());
}
/**
* Render license status notice
*/
private function renderLicenseStatus(): void
{
$checker = PluginLicenseChecker::getInstance();
if ($checker->isLocalhost()) {
echo '<div class="notice notice-info inline"><p>';
echo '<span class="dashicons dashicons-info" style="color: #00a0d2;"></span> ';
echo esc_html__('Running on localhost - license validation bypassed.', 'wc-licensed-product');
echo '</p></div>';
return;
}
if ($checker->isLicenseValid()) {
echo '<div class="notice notice-success inline"><p>';
echo '<span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span> ';
echo esc_html__('License is valid and active.', 'wc-licensed-product');
echo '</p></div>';
} else {
$error = $checker->getLastError();
echo '<div class="notice notice-error inline"><p>';
echo '<span class="dashicons dashicons-warning" style="color: #dc3232;"></span> ';
echo esc_html__('License is not valid. Frontend features are disabled.', 'wc-licensed-product');
if ($error) {
echo '<br><small>' . esc_html($error) . '</small>';
}
echo '</p></div>';
}
// Add verify button
$nonce = wp_create_nonce('wclp_verify_license');
echo '<p>';
echo '<button type="button" class="button" id="wclp-verify-license" data-nonce="' . esc_attr($nonce) . '">';
echo esc_html__('Verify License', 'wc-licensed-product');
echo '</button>';
echo '<span id="wclp-verify-result" style="margin-left: 10px;"></span>';
echo '</p>';
// Inline script for verify button
?>
<script type="text/javascript">
jQuery(function($) {
$('#wclp-verify-license').on('click', function() {
var $btn = $(this);
var $result = $('#wclp-verify-result');
var nonce = $btn.data('nonce');
$btn.prop('disabled', true).text('<?php echo esc_js(__('Verifying...', 'wc-licensed-product')); ?>');
$result.text('');
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wclp_verify_plugin_license',
nonce: nonce
},
success: function(response) {
if (response.success) {
$result.html('<span style="color: #46b450;">' + response.data.message + '</span>');
location.reload();
} else {
$result.html('<span style="color: #dc3232;">' + response.data.message + '</span>');
}
},
error: function() {
$result.html('<span style="color: #dc3232;"><?php echo esc_js(__('Request failed.', 'wc-licensed-product')); ?></span>');
},
complete: function() {
$btn.prop('disabled', false).text('<?php echo esc_js(__('Verify License', 'wc-licensed-product')); ?>');
}
});
});
});
</script>
<?php
}
/**
* Save settings
*/
@@ -210,4 +419,55 @@ final class SettingsController
$value = get_option('wc_licensed_product_expiration_warning_days_second', 1);
return max(0, (int) $value);
}
/**
* Get plugin license server URL
*/
public static function getPluginLicenseServerUrl(): string
{
return (string) get_option('wc_licensed_product_plugin_license_server_url', '');
}
/**
* Get plugin license key
*/
public static function getPluginLicenseKey(): string
{
return (string) get_option('wc_licensed_product_plugin_license_key', '');
}
/**
* Get plugin license server secret
*/
public static function getPluginLicenseServerSecret(): ?string
{
$secret = get_option('wc_licensed_product_plugin_license_server_secret', '');
return !empty($secret) ? (string) $secret : null;
}
/**
* Handle AJAX verify license request
*/
public function handleVerifyLicense(): void
{
if (!check_ajax_referer('wclp_verify_license', 'nonce', false)) {
wp_send_json_error(['message' => __('Security check failed.', 'wc-licensed-product')], 403);
}
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Insufficient permissions.', 'wc-licensed-product')], 403);
}
$checker = PluginLicenseChecker::getInstance();
$checker->clearCache();
$valid = $checker->validateLicense(true);
if ($valid) {
wp_send_json_success(['message' => __('License verified successfully!', 'wc-licensed-product')]);
} else {
$error = $checker->getLastError() ?: __('License validation failed.', 'wc-licensed-product');
wp_send_json_error(['message' => $error]);
}
}
}