diff --git a/src/LicenseHandler.php b/src/LicenseHandler.php index a93f519..62b29e0 100644 --- a/src/LicenseHandler.php +++ b/src/LicenseHandler.php @@ -4,7 +4,6 @@ * * @package gravitywp-license-handler * @license MIT - * */ namespace GravityWP\LicenseHandler; @@ -17,9 +16,10 @@ /** * Handles GWP Licenses. * - * @version 2.0.3 + * @version 2.0.5 */ class LicenseHandler { + /** * Update endpoint of the API * @@ -53,7 +53,7 @@ class LicenseHandler { * * @var mixed|string */ - private $version = ''; + private $version = '2.1.0'; /** * WP Override flag @@ -129,13 +129,13 @@ class LicenseHandler { private $_addon_license = ''; /** - * Store the GravityWP GF Addon license hash. + * Store the GravityWP Global License Key. * * @since 1.0 * @access private - * @var string $_addon_license_hash the GravityWP GF Addon license hash. + * @var string $_global_license_key Global License Key. */ - private $_addon_license_hash = ''; + private $_global_license_key = ''; /** * Store the GravityWP GF Addon title @@ -167,56 +167,71 @@ class LicenseHandler { /** * Constructor. * - * @since 1.0 + * @since 1.0 * - * @param string $gwp_addon_class GravityWP GF Addon classname. + * @param string $gwp_addon_class GravityWP GF Addon classname. * @param string $plugin_file_path Path to main plugin file. * * @return void */ public function __construct( $gwp_addon_class, $plugin_file_path ) { + // Load the loader class (only once). + if ( ! class_exists( '\GravityWP\Shared\Global_License_Key_Loader' ) ) { + require_once __DIR__ . '/shared/class-global-license-key-loader.php'; + } + + if ( $this->version ) { + // Register this plugin’s version. + \GravityWP\Shared\Global_License_Key_Loader::register( $this->version, __DIR__ . '/shared/class-global-license-key-registry.php', $gwp_addon_class ); + } + $doing_cron = defined( 'DOING_CRON' ) && DOING_CRON; - if ( ! current_user_can( 'manage_options' ) && ! $doing_cron ) { + if ( ! ( current_user_can( 'gform_full_access' ) || current_user_can( 'gravityforms_edit_settings' ) || current_user_can( 'gravityforms_view_settings' ) ) && ! $doing_cron ) { return; } - $this->_addon_class = $gwp_addon_class; - $this->_addon_file_path = $plugin_file_path; - $this->_addon_slug = $gwp_addon_class::get_instance()->get_slug(); - $this->_addon_license = $gwp_addon_class::get_instance()->get_plugin_setting( $this->_addon_slug . '_license_key' ); - $this->_addon_title = $gwp_addon_class::get_instance()->plugin_page_title(); + $this->_addon_class = $gwp_addon_class; + $this->_addon_file_path = $plugin_file_path; + $this->_addon_slug = $gwp_addon_class::get_instance()->get_slug(); + $this->_addon_license = $gwp_addon_class::get_instance()->get_plugin_setting( $this->_addon_slug . '_license_key' ); + $this->_addon_title = $gwp_addon_class::get_instance()->plugin_page_title(); + $this->_global_license_key = get_option( 'gravitywp_global_license_key', '' ); $this->initialize_paddlepress_client(); } /** * Initialize or reinitialize the Paddlepress client. * - * @return bool + * @param string|null $field_setting Optional license key override from a field or custom setting. + * @return bool True if initialization succeeded, false otherwise. */ public function initialize_paddlepress_client( $field_setting = null ) { try { unset( $this->_paddlepress_client ); unset( $this->_license_handler ); - $license_key = ! empty( $field_setting ) ? $field_setting : $this->_addon_license; + $license_key = ! empty( $field_setting ) ? $field_setting : $this->_addon_license; + + if ( empty( $license_key ) ) { + $license_key = $this->_global_license_key; + } $this->_license_handler = new Plugin_Updater( $this->_addon_file_path, array( - 'version' => $this->_addon_class::get_instance()->get_version(), // current version number. - 'license_key' => $license_key, // license key (used get_option above to retrieve from DB)..'error' - 'license_url' => home_url(), // license domain. - 'download_tag' => $this->_addon_slug, // download tag slug. - 'beta' => false, + 'version' => $this->_addon_class::get_instance()->get_version(), // current version number. + 'license_key' => $license_key, // license key (used get_option above to retrieve from DB)..'error'. + 'license_url' => home_url(), // license domain. + 'download_tag' => $this->_addon_slug, // download tag slug. + 'beta' => false, 'handler_class' => $this, ) ); $use_cached_info = ! empty( $field_setting ) ? false : true; if ( $this->_license_handler->gwp_is_valid( $use_cached_info, $license_key ) ) { - remove_action( 'admin_notices', array( $this, 'action_admin_notices' ) ); + remove_action( 'admin_notices', array( $this, 'action_admin_notices' ) ); } else { add_action( 'admin_notices', array( $this, 'action_admin_notices' ) ); } - } catch ( \Exception $e ) { $this->_addon_class::get_instance()->log_error( __CLASS__ . '::' . __METHOD__ . '(): License client failed to initialize: ' . $e->getMessage() ); return false; @@ -228,7 +243,7 @@ public function initialize_paddlepress_client( $field_setting = null ) { /** * Display an admin notice. * - * @since 1.0 + * @since 1.0 * * @return void */ @@ -253,29 +268,77 @@ public function action_admin_notices() { /** * Define plugin settings fields. * - * @since 1.0 + * @since 1.0 * * @return array */ public function plugin_settings_license_fields() { - $this->_addon_license = $this->_addon_class::get_instance()->get_plugin_setting( $this->_addon_slug . '_license_key' ); + $this->_addon_license = $this->_addon_class::get_instance()->get_plugin_setting( + $this->_addon_slug . '_license_key' + ); + + $license_key_name = $this->_addon_slug . '_license_key'; + // Main license input field. $license_field = array( - 'name' => $this->_addon_slug . '_license_key', - 'tooltip' => esc_html__( 'Enter the license key you received after purchasing the plugin.', 'gravitywp-license-handler' ), - 'label' => esc_html__( 'License Key', 'gravitywp-license-handler' ), - 'type' => 'text', - 'input_type' => 'password', - 'class' => 'medium', - 'default_value' => '', - 'required' => true, - 'validation_callback' => array( $this, 'license_validation' ), - 'feedback_callback' => array( $this, 'license_feedback' ), - 'error_message' => esc_html__( 'Invalid or expired license', 'gravitywp-license-handler' ), + 'title' => esc_html__( 'To unlock plugin updates and support, please enter your license key below.', 'gravitywp-license-handler' ), + 'fields' => array( + array( + 'name' => $license_key_name, + 'label' => esc_html__( 'Plugin License Key', 'gravitywp-license-handler' ), + 'tooltip' => esc_html__( 'Enter the license key you received after purchasing the plugin.', 'gravitywp-license-handler' ), + 'type' => 'text', + 'input_type' => 'password', + 'class' => 'medium', + 'default_value' => '', + 'required' => false, + 'validation_callback' => array( $this, 'license_validation' ), + 'feedback_callback' => array( $this, 'license_feedback' ), + 'error_message' => esc_html__( 'Invalid or expired license.', 'gravitywp-license-handler' ), + ), + ), ); - $license_field['title'] = esc_html__( 'To unlock plugin updates and support, please enter your license key below', 'gravitywp-license-handler' ); - $license_field['fields'] = array( $license_field ); + // Determine current license state. + if ( isset( $_POST[ '_gform_setting_' . $this->_addon_slug . '_license_key' ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + // If the form is submitted, use the posted value. + $plugin_license_key = rgpost( '_gform_setting_' . $this->_addon_slug . '_license_key' ); + } else { + // Otherwise, use the stored value. + $plugin_license_key = $this->_addon_license ?? ''; + } + $global_license_key = $this->_global_license_key ?? ''; + + // Add contextual note based on key presence. + $license_field['fields'][] = array( + 'name' => 'license_note', + 'type' => 'html', + 'html' => function () use ( $plugin_license_key, $global_license_key ) { + $global_settings_url = admin_url( 'admin.php?page=gravitywp-settings' ); + $global_settings_text = esc_html__( 'Global License Key', 'gravitywp-license-handler' ); + $message_color = 'inherit'; + if ( ! empty( $plugin_license_key ) && ! empty( $global_license_key ) ) { + /* translators: %s: link to global settings */ + $message = sprintf( esc_html__( 'This Plugin License Key overrides the %s. To use the global key, leave this field empty.', 'gravitywp-license-handler' ), '' . esc_html( $global_settings_text ) . '' ); + } elseif ( empty( $plugin_license_key ) && ! empty( $global_license_key ) ) { + /* translators: %s: link to global settings */ + if ( $this->_license_handler->gwp_is_valid( true, $global_license_key ) ) { + /* translators: %s: link to global settings */ + $message = sprintf( esc_html__( 'A %s is active. If needed you can override the Global Key with the Plugin Key.', 'gravitywp-license-handler' ), '' . esc_html( $global_settings_text ) . '' ); + $message_color = 'green'; + } else { + /* translators: %s: link to global settings, link to gravitywp.com */ + $message = sprintf( esc_html__( 'A %1$s is active, but it is not valid for this addon. You can override the Global Key with a specific Plugin Key or purchase an All Access license on %2$s.', 'gravitywp-license-handler' ), '' . esc_html( $global_settings_text ) . '', 'gravitywp.com' ); + $message_color = 'red'; + } + } else { + $message_color = 'red'; + /* translators: %s: link to global settings */ + $message = sprintf( esc_html__( 'No active license. If you have an All Access License you can set up a %s.', 'gravitywp-license-handler' ), '' . esc_html( $global_settings_text ) . '' ); + } + return '' . $message . ''; + }, + ); return $license_field; } @@ -283,9 +346,9 @@ public function plugin_settings_license_fields() { /** * Handle license key activation or deactivation and on save the settings. * - * @since 1.0 + * @since 1.0 * - * @param array $field The field properties. + * @param array $field The field properties. * @param string $field_setting The submitted value of the license_key field. */ public function license_validation( $field, $field_setting ) { @@ -310,7 +373,7 @@ public function license_validation( $field, $field_setting ) { * @param string $value The current value of the license_key field. * @param array $field The field properties. * - * @since 1.0 + * @since 1.0 * * @return bool|null */ diff --git a/src/changelog.txt b/src/changelog.txt index 48075db..51cced6 100644 --- a/src/changelog.txt +++ b/src/changelog.txt @@ -1,3 +1,9 @@ += 2.1.0 = +- Implementing the global licensed key. + += 2.0.5 = +- Fix non unique #id in plugin setting. + = 2.0.4 = - Optimizing the validation function caches the result. diff --git a/src/pluginUpdater.php b/src/pluginUpdater.php index f3967a3..1d69f4c 100644 --- a/src/pluginUpdater.php +++ b/src/pluginUpdater.php @@ -224,7 +224,36 @@ public function request_is_activate( $field_setting ) { $this->error_messages = nl2br( $this->generateErrorMessage( $json_data['message'] ) ); return false; } else { - $this->error_messages = nl2br( $this->generateErrorMessage( 'Please try again later. If the issue persists, please contact support.' ) ); + // Retrieve HTTP status code and headers for additional context. + $http_code = wp_remote_retrieve_response_code( $response ); + $headers = wp_remote_retrieve_headers( $response ); + + // Check for block by Cloudflare. + if ( $http_code === 403 && strpos( strtolower( $body ), 'cloudflare' ) !== false ) { + $this->error_messages = nl2br( + $this->generateErrorMessage( + 'Access to the license server was denied by Cloudflare. This happens when malicious activity was detected from your website\'s outgoing IP address. This often happens on shared hosting where other users use the same IP address for malicious activity. Contact your hosting provider to resolve this issue. For more information, visit this page.' + ) + ); + return false; + } + + // Format additional information into a string for logging or support use. + $extra_info = sprintf( + "HTTP Status Code: %d\nResponse Headers: %s", + $http_code, + json_encode( $headers ) + ); + + // Provide a detailed error message including the response code, extra info, and a reference URL. + $this->error_messages = nl2br( + $this->generateErrorMessage( + sprintf( + 'An unexpected error occurred. Please try again later. You can also refer to this page for more details. If the issue persists, provide the following information to support: %s.', + esc_html( $extra_info ) // Ensure special characters in the info are safely included in an HTML context. + ) + ) + ); return false; } } @@ -260,7 +289,7 @@ public function generateErrorMessage( $error ) { } } else { $sanitized_message = sanitize_text_field( $messages ); - $error_message .= "- $sanitized_message\n"; + $error_message .= "- $sanitized_message\n"; } } } else { @@ -325,13 +354,13 @@ public function gwp_is_valid( $cached, $key = null ) { /** * Checks the plugin's license status and updates the transient data accordingly. * - * This method verifies the validity of the plugin's license key and updates the cached - * license status. It also manages admin notices based on the validation result. If the - * license key is valid, it removes any existing admin notices. Otherwise, it adds an + * This method verifies the validity of the plugin's license key and updates the cached + * license status. It also manages admin notices based on the validation result. If the + * license key is valid, it removes any existing admin notices. Otherwise, it adds an * admin notice to inform the user about the invalid license status. * * Key Details: - * - Verifies if the current context is not the plugins page in a multisite network, + * - Verifies if the current context is not the plugins page in a multisite network, * and returns early if true. * - Checks existing transient data for license status response, and skips further checks * if the data is already populated unless overridden. @@ -351,37 +380,37 @@ public function gwp_is_valid( $cached, $key = null ) { * @uses set_version_info_cache() Caches the result of the license key validation. * @uses gwp_is_valid() Validates the license key using cached data or via API. */ - public function check_update_license($_transient_data) { + public function check_update_license( $_transient_data ) { global $pagenow; // Ensure $_transient_data is an object. - if (!is_object($_transient_data)) { + if ( ! is_object( $_transient_data ) ) { $_transient_data = new stdClass(); } // Return early if on the plugins page in a multisite network. - if ('plugins.php' === $pagenow && is_multisite()) { + if ( 'plugins.php' === $pagenow && is_multisite() ) { return $_transient_data; } // Check if the transient data already has a response and is not being overridden. - if (!empty($_transient_data->response) && !empty($_transient_data->response[$this->name]) && false === $this->wp_override) { + if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) { return $_transient_data; } // Retrieve license key and generate a unique cache key. $license_key = $this->api_data['license_key']; - $status_cache_key = 'paddlepress_status_request_' . md5(serialize($this->slug . $this->api_data['license_key'] . $this->beta)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $status_cache_key = 'paddlepress_status_request_' . md5( serialize( $this->slug . $this->api_data['license_key'] . $this->beta ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize // Validate the license key through the API and update the cache. - $status = $this->request_is_activate($license_key); - $this->set_version_info_cache($status, $status_cache_key); + $status = $this->request_is_activate( $license_key ); + $this->set_version_info_cache( $status, $status_cache_key ); // Validate the license key and manage admin notices based on validity. - if ($this->gwp_is_valid(false, $license_key)) { - remove_action('admin_notices', array($this->handler_class, 'action_admin_notices')); + if ( $this->gwp_is_valid( false, $license_key ) ) { + remove_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) ); } else { - add_action('admin_notices', array($this->handler_class, 'action_admin_notices')); + add_action( 'admin_notices', array( $this->handler_class, 'action_admin_notices' ) ); } return $_transient_data; @@ -522,7 +551,7 @@ public function show_update_notification( $file, $plugin ) { if ( empty( $update_cache->response ) || empty( $update_cache->response[ $this->name ] ) ) { - $version_info = $this->get_cached_version_info(); // || false; + $version_info = $this->get_cached_version_info(); // || false; if ( false === $version_info ) { $version_info = $this->api_request( diff --git a/src/shared/class-global-license-key-loader.php b/src/shared/class-global-license-key-loader.php new file mode 100644 index 0000000..0e35c8d --- /dev/null +++ b/src/shared/class-global-license-key-loader.php @@ -0,0 +1,86 @@ + $version, + 'file' => $file_path, + 'addon_class' => $gwp_addon_class, + ); + } + /** + * Loads the last (highest) version from the registered candidates. + * + * @return void + */ + public static function load_last_version() { + if ( empty( self::$candidates ) ) { + return; + } + + /* + * Sort candidates in descending order by version number. + */ + usort( + self::$candidates, + function ( $a, $b ) { + return version_compare( $b['version'], $a['version'] ); + } + ); + + $last = self::$candidates[0]; + require_once $last['file']; + + if ( class_exists( '\GravityWP\Shared\Global_License_Key_Registry' ) ) { + \GravityWP\Shared\Global_License_Key_Registry::init( $last['version'] ); // Load the UI and logic. + } + } + /** + * Returns the candidates array. + * + * @return array + */ + public static function get_registered_license_handlers() { + return self::$candidates; + } + } + + add_action( 'init', array( '\GravityWP\Shared\Global_License_Key_Loader', 'load_last_version' ), 999 ); +} diff --git a/src/shared/class-global-license-key-registry.php b/src/shared/class-global-license-key-registry.php new file mode 100644 index 0000000..34c3e99 --- /dev/null +++ b/src/shared/class-global-license-key-registry.php @@ -0,0 +1,321 @@ + $global_key, + 'license_url' => home_url(), + 'action' => 'activate', + 'glk_version' => esc_html( self::$version ), + ), + 'https://my.gravitywp.com/wp-json/paddlepress-api/v1/license' + ); + + $response = wp_remote_get( esc_url_raw( $api_url ), array( 'timeout' => 15 ) ); + + if ( is_wp_error( $response ) ) { + $error_message = 'Network Error: ' . $response->get_error_message(); + } else { + $code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + $headers = wp_remote_retrieve_headers( $response ); + + // Check for block by Cloudflare. + if ( $code === 403 && strpos( strtolower( $body ), 'cloudflare' ) !== false ) { + $error_message = nl2br( 'Access to the license server was denied by Cloudflare. This happens when malicious activity was detected from your website\'s outgoing IP address. This often happens on shared hosting where other users use the same IP address for malicious activity. Contact your hosting provider to resolve this issue.' ); + } else { + switch ( $code ) { + case 200: + $decoded = json_decode( $body, true ); + if ( is_array( $decoded ) && ! empty( $decoded['success'] ) && $decoded['license_status'] === 'valid' ) { + $data = $decoded; + if ( class_exists( '\GravityWP\Shared\Global_License_Key_Loader' ) ) { + $gwp_addons = \GravityWP\Shared\Global_License_Key_Loader::get_registered_license_handlers(); + foreach ( $gwp_addons as $gwp_addon ) { + $gwp_addon_class = $gwp_addon['addon_class'] ?? ''; + if ( $gwp_addon_class && class_exists( $gwp_addon_class ) ) { + $gwp_addon_slug = $gwp_addon_class::get_instance()->get_slug(); + \GFCommon::remove_dismissible_message( $gwp_addon_slug . '_license_message_notice' ); + } + } + } + } else { + $error_message = 'Invalid license.'; + self::extract_error_details( $decoded, $error_details ); + } + break; + case 500: + $error_message = 'Server Error: The server encountered an internal error. Please try again later.'; + break; + default: + // Format additional information into a string for logging or support use. + $extra_info = sprintf( + "HTTP Status Code: %d\nResponse Headers: %s", + $code, + wp_json_encode( $headers ) + ); + $error_message = nl2br( sprintf( 'An unexpected error occurred. Please try again later. If the issue persists, provide the following information to support: %s', esc_html( $extra_info ) ) ); + self::extract_error_details( json_decode( $body, true ), $error_details ); + break; + } + } + } + } + ?> +
+

+ + + () + +

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ +

+
+
+
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +

+
+ +
+ +
+ +
+ +
+ + $messages ) { + foreach ( (array) $messages as $msg ) { + $details[] = esc_html( $msg ); + } + } + $error_details = implode( '
', $details ); + } + } +} diff --git a/src/shared/css/gwp-styles.css b/src/shared/css/gwp-styles.css new file mode 100644 index 0000000..daec9f6 --- /dev/null +++ b/src/shared/css/gwp-styles.css @@ -0,0 +1,195 @@ +/* === General Container Box (for license info or input box) === */ +.gwp-settings-box, +.gwp-license-box { + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 30px; + max-width: 600px; + margin-top: 30px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05); + font-family: "Segoe UI", Tahoma, sans-serif; +} + +/* === License Info Box === */ +.gwp-license-header { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.gwp-license-header .dashicons { + font-size: 22px; + color: #10b981; + margin-right: 10px; +} + +.gwp-license-header h2 { + margin: 0; + font-size: 20px; + color: #1f2937; +} + +.gwp-license-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px 20px; +} + +.gwp-license-item { + display: flex; + flex-direction: column; +} + +.gwp-license-item .label { + font-size: 14px; + color: #6b7280; + margin-bottom: 4px; +} + +.gwp-license-item .value { + font-size: 16px; + font-weight: 600; + color: #111827; +} + +.gwp-license-item .value.success { + color: #10b981; +} + +.gwp-license-item .value.error { + color: #ef4444; +} + +/* === License Input Field === */ +.gwp-license-key-row { + display: flex; + flex-direction: column; + margin: 20px 0; +} + +.gwp-license-label { + font-size: 15px; + font-weight: 600; + color: #374151; + margin-bottom: 8px; +} + +.gwp-license-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.gwp-license-input { + padding: 12px 40px 12px 16px; + font-size: 16px; + border-radius: 8px; + border: 1px solid #d1d5db; + background-color: #fff; + width: 100%; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.gwp-toggle-visibility { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + border: none; + background: none; + font-size: 20px; + padding: 0; + cursor: pointer; + color: #888; + line-height: 1; +} + +.gwp-toggle-visibility:hover { + color: #111; +} + +/* === Error Message Box === */ +.gwp-license-error-box { + background: #fff0f0; + border: 1px solid #dc3232; + padding: 20px 24px; + border-radius: 10px; + max-width: 600px; + margin-top: 30px; + font-family: "Segoe UI", Tahoma, sans-serif; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.gwp-license-error-header { + display: flex; + align-items: center; + color: #dc3232; +} + +.gwp-license-error-header .dashicons { + font-size: 20px; + margin-right: 8px; +} + +.gwp-license-error-header h2 { + margin: 0; + font-size: 18px; + color: #dc3232; + font-weight: 600; +} + +.gwp-license-error-details { + margin-top: 10px; + color: #a00; + font-size: 14px; +} + + +.password-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.gwp-license-input { + padding: 8px 40px 8px 10px; + /* Add padding to the right for the button */ + font-size: 14px; + line-height: 1.5; + width: 100%; + border: 1px solid #8c8f94; + border-radius: 4px; + box-sizing: border-box; + /* Ensures padding doesn't affect the final width */ +} + +.toggle-visibility-button { + position: absolute; + right: 1px; + top: 1px; + bottom: 1px; + border: none; + background: transparent; + cursor: pointer; + padding: 0 10px; + color: #50575e; + display: flex; + align-items: center; + justify-content: center; +} + +.toggle-visibility-button:hover { + color: #2271b1; +} + +.toggle-visibility-button svg { + width: 20px; + height: 20px; +} + +.gwp_settings_version { + font-size: 0.6em; + color: #666; + margin-left: 10px; +} \ No newline at end of file diff --git a/src/shared/css/gwp-styles.min.css b/src/shared/css/gwp-styles.min.css new file mode 100644 index 0000000..44cfdb3 --- /dev/null +++ b/src/shared/css/gwp-styles.min.css @@ -0,0 +1 @@ +.gwp-settings-box,.gwp-license-box{background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;padding:30px;max-width:600px;margin-top:30px;box-shadow:0 1px 4px rgba(0,0,0,.05);font-family:"Segoe UI",Tahoma,sans-serif;}.gwp-license-header{display:flex;align-items:center;margin-bottom:20px;}.gwp-license-header .dashicons{font-size:22px;color:#10b981;margin-right:10px;}.gwp-license-header h2{margin:0;font-size:20px;color:#1f2937;}.gwp-license-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px 20px;}.gwp-license-item{display:flex;flex-direction:column;}.gwp-license-item .label{font-size:14px;color:#6b7280;margin-bottom:4px;}.gwp-license-item .value{font-size:16px;font-weight:600;color:#111827;}.gwp-license-item .value.success{color:#10b981;}.gwp-license-item .value.error{color:#ef4444;}.gwp-license-key-row{display:flex;flex-direction:column;margin:20px 0;}.gwp-license-label{font-size:15px;font-weight:600;color:#374151;margin-bottom:8px;}.gwp-license-input-wrapper{position:relative;display:flex;align-items:center;}.gwp-license-input{padding:12px 40px 12px 16px;font-size:16px;border-radius:8px;border:1px solid #d1d5db;background-color:#fff;width:100%;box-shadow:inset 0 1px 2px rgba(0,0,0,.05);}.gwp-toggle-visibility{position:absolute;right:12px;top:50%;transform:translateY(-50%);border:none;background:none;font-size:20px;padding:0;cursor:pointer;color:#888;line-height:1;}.gwp-toggle-visibility:hover{color:#111;}.gwp-license-error-box{background:#fff0f0;border:1px solid #dc3232;padding:20px 24px;border-radius:10px;max-width:600px;margin-top:30px;font-family:"Segoe UI",Tahoma,sans-serif;box-shadow:0 1px 3px rgba(0,0,0,.05);}.gwp-license-error-header{display:flex;align-items:center;color:#dc3232;}.gwp-license-error-header .dashicons{font-size:20px;margin-right:8px;}.gwp-license-error-header h2{margin:0;font-size:18px;color:#dc3232;font-weight:600;}.gwp-license-error-details{margin-top:10px;color:#a00;font-size:14px;}.password-input-wrapper{position:relative;display:flex;align-items:center;}.gwp-license-input{padding:8px 40px 8px 10px;font-size:14px;line-height:1.5;width:100%;border:1px solid #8c8f94;border-radius:4px;box-sizing:border-box}.toggle-visibility-button{position:absolute;right:1px;top:1px;bottom:1px;border:none;background:transparent;cursor:pointer;padding:0 10px;color:#50575e;display:flex;align-items:center;justify-content:center;}.toggle-visibility-button:hover{color:#2271b1;}.toggle-visibility-button svg{width:20px;height:20px;}.gwp_settings_version{font-size:.6em;color:#666;margin-left:10px;} \ No newline at end of file