commit 271960206cd5be5273e51d65a8a529f4452ec5e8
Author: Yuri
' . esc_html__('Configure how prices are displayed on your website.', 'whmcs-pricing') . '
'; + } + + public function render_performance_section() { + echo '' . esc_html__('Optimize plugin performance with caching.', 'whmcs-pricing') . '
'; + } + + public function render_advanced_section() { + echo '' . esc_html__('Advanced options for troubleshooting and development.', 'whmcs-pricing') . '
'; + } + + /** + * Field rendering methods + */ + public function render_text_field($args) { + $settings = get_option('whmcs_pricing_settings', array()); + $value = isset($settings[$args['name']]) ? $settings[$args['name']] : ''; + $placeholder = isset($args['placeholder']) ? $args['placeholder'] : ''; + ?> + + + + + + + + + + + + + + + + + 'USD - US Dollar ($)', + 'EUR' => 'EUR - Euro (€)', + 'GBP' => 'GBP - British Pound (£)', + 'AUD' => 'AUD - Australian Dollar (A$)', + 'CAD' => 'CAD - Canadian Dollar (C$)', + 'JPY' => 'JPY - Japanese Yen (¥)', + 'CNY' => 'CNY - Chinese Yuan (¥)', + 'INR' => 'INR - Indian Rupee (₹)', + 'BRL' => 'BRL - Brazilian Real (R$)', + 'RUB' => 'RUB - Russian Ruble (₽)', + 'CHF' => 'CHF - Swiss Franc (Fr)', + 'SEK' => 'SEK - Swedish Krona (kr)', + 'NOK' => 'NOK - Norwegian Krone (kr)', + 'DKK' => 'DKK - Danish Krone (kr)', + 'PLN' => 'PLN - Polish Zloty (zł)', + 'THB' => 'THB - Thai Baht (฿)', + 'MYR' => 'MYR - Malaysian Ringgit (RM)', + 'SGD' => 'SGD - Singapore Dollar (S$)', + 'NZD' => 'NZD - New Zealand Dollar (NZ$)', + 'MXN' => 'MXN - Mexican Peso (Mex$)', + 'ZAR' => 'ZAR - South African Rand (R)' + ); + ?> + + + + + 'whmcs-pricing', + 'cache-cleared' => 'true' + ), + admin_url('options-general.php') + )); + exit; + } + + /** + * Enqueue admin styles + */ + public function enqueue_admin_styles($hook) { + if ('settings_page_whmcs-pricing' !== $hook) { + return; + } + + wp_enqueue_style( + 'whmcs-pricing-admin', + WHMCS_PRICING_PLUGIN_URL . 'assets/css/admin.css', + array(), + WHMCS_PRICING_VERSION + ); + } +} diff --git a/whmcs-pricing-plugin/includes/api-handler.php b/whmcs-pricing-plugin/includes/api-handler.php new file mode 100644 index 0000000..418344a --- /dev/null +++ b/whmcs-pricing-plugin/includes/api-handler.php @@ -0,0 +1,316 @@ + '', + 'api_identifier' => '', + 'api_secret' => '', + 'cache_duration' => 3600, + 'default_currency' => 'USD', + 'show_decimals' => true, + 'debug_mode' => false + ); + + $settings = get_option('whmcs_pricing_settings', $defaults); + return wp_parse_args($settings, $defaults); + } + + /** + * Make API request to WHMCS + */ + private static function make_request($action, $params = array()) { + $settings = self::get_settings(); + + // Validate settings + if (empty($settings['api_url']) || empty($settings['api_identifier']) || empty($settings['api_secret'])) { + self::log_error('API credentials not configured'); + return new WP_Error('missing_credentials', __('WHMCS API credentials not configured', 'whmcs-pricing')); + } + + // Prepare API request + $api_url = rtrim($settings['api_url'], '/') . '/includes/api.php'; + + $post_data = array_merge(array( + 'action' => $action, + 'identifier' => $settings['api_identifier'], + 'secret' => $settings['api_secret'], + 'responsetype' => 'json' + ), $params); + + // Make request + $response = wp_remote_post($api_url, array( + 'timeout' => 15, + 'body' => $post_data, + 'sslverify' => true + )); + + // Check for errors + if (is_wp_error($response)) { + self::log_error('API request failed: ' . $response->get_error_message()); + return $response; + } + + // Parse response + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + self::log_error('Invalid JSON response from WHMCS API'); + return new WP_Error('invalid_response', __('Invalid response from WHMCS API', 'whmcs-pricing')); + } + + // Check API result + if (isset($data['result']) && $data['result'] === 'error') { + $error_message = isset($data['message']) ? $data['message'] : __('Unknown API error', 'whmcs-pricing'); + self::log_error('WHMCS API error: ' . $error_message); + return new WP_Error('api_error', $error_message); + } + + return $data; + } + + /** + * Get product details + */ + public static function get_product($product_id) { + $cache_key = 'whmcs_product_' . $product_id; + $cached_data = get_transient($cache_key); + + if (false !== $cached_data && !self::is_debug_mode()) { + return $cached_data; + } + + // Make API request + $response = self::make_request('GetProducts', array( + 'pid' => $product_id + )); + + if (is_wp_error($response)) { + return $response; + } + + // Extract product data + if (!isset($response['products']) || !isset($response['products']['product'])) { + self::log_error('Product not found: ' . $product_id); + return new WP_Error('product_not_found', __('Product not found', 'whmcs-pricing')); + } + + $product_data = is_array($response['products']['product']) && isset($response['products']['product'][0]) + ? $response['products']['product'][0] + : $response['products']['product']; + + // Debug log the product data structure + if (self::is_debug_mode()) { + self::log_error('Product data for ID ' . $product_id . ': ' . print_r($product_data, true)); + } + + // Cache the result + $settings = self::get_settings(); + set_transient($cache_key, $product_data, absint($settings['cache_duration'])); + + return $product_data; + } + + /** + * Get product price + */ + public static function get_price($product_id, $args = array()) { + $defaults = array( + 'billing_cycle' => 'monthly', + 'currency' => null, + 'format' => 'default' + ); + + $args = wp_parse_args($args, $defaults); + $settings = self::get_settings(); + + // Use default currency if not specified + if (empty($args['currency'])) { + $args['currency'] = $settings['default_currency']; + } + + // Get product data + $product = self::get_product($product_id); + + if (is_wp_error($product)) { + return $product; + } + + // Find pricing for the requested currency and billing cycle + $price = self::extract_price($product, $args['billing_cycle'], $args['currency']); + + if (is_wp_error($price)) { + return $price; + } + + // Format the price + return self::format_price($price, $args['currency'], $args['format']); + } + + /** + * Extract price from product data + */ + private static function extract_price($product, $billing_cycle, $currency) { + // Normalize billing cycle + $cycle_map = array( + 'monthly' => 'monthly', + 'quarterly' => 'quarterly', + 'semiannually' => 'semiannually', + 'annually' => 'annually', + 'biennially' => 'biennially', + 'triennially' => 'triennially', + 'month' => 'monthly', + 'quarter' => 'quarterly', + 'year' => 'annually', + 'annual' => 'annually' + ); + + $billing_cycle = strtolower($billing_cycle); + $billing_cycle = isset($cycle_map[$billing_cycle]) ? $cycle_map[$billing_cycle] : $billing_cycle; + + // Debug pricing structure + if (self::is_debug_mode()) { + self::log_error("Looking for price: currency={$currency}, cycle={$billing_cycle}"); + if (isset($product['pricing'])) { + self::log_error('Available pricing: ' . print_r($product['pricing'], true)); + } + } + + // Look for pricing in product data + if (isset($product['pricing'][$currency][$billing_cycle])) { + $price = floatval($product['pricing'][$currency][$billing_cycle]); + + // WHMCS uses -1.00 to indicate billing cycle not available + if ($price < 0) { + if (self::is_debug_mode()) { + self::log_error("Price is -1 (not available) for cycle: {$billing_cycle}"); + } + return new WP_Error('price_not_available', __('This billing cycle is not available for this product', 'whmcs-pricing')); + } + + if (self::is_debug_mode()) { + self::log_error("Found price: {$price}"); + } + return $price; + } + + // Try to find any available pricing and log it + if (self::is_debug_mode() && isset($product['pricing'])) { + self::log_error("Available currencies: " . implode(', ', array_keys($product['pricing']))); + if (isset($product['pricing'][$currency])) { + self::log_error("Available cycles for {$currency}: " . implode(', ', array_keys($product['pricing'][$currency]))); + } + } + + self::log_error("Price not found for product {$product['pid']}, cycle: {$billing_cycle}, currency: {$currency}"); + return new WP_Error('price_not_found', __('Price not available for selected options', 'whmcs-pricing')); + } + + /** + * Format price + */ + private static function format_price($price, $currency, $format) { + $settings = self::get_settings(); + + // Apply decimal formatting + if ($format === 'no_decimals' || !$settings['show_decimals']) { + $price = round($price); + $formatted_price = number_format($price, 0); + } else { + $formatted_price = number_format($price, 2); + } + + // Get currency symbol + $currency_symbol = self::get_currency_symbol($currency); + + return array( + 'raw' => $price, + 'formatted' => $formatted_price, + 'currency' => $currency, + 'symbol' => $currency_symbol + ); + } + + /** + * Get currency symbol + */ + private static function get_currency_symbol($currency) { + $symbols = array( + 'USD' => '$', + 'EUR' => '€', + 'GBP' => '£', + 'AUD' => 'A$', + 'CAD' => 'C$', + 'JPY' => '¥', + 'CNY' => '¥', + 'INR' => '₹', + 'BRL' => 'R$', + 'RUB' => '₽', + 'CHF' => 'Fr', + 'SEK' => 'kr', + 'NOK' => 'kr', + 'DKK' => 'kr', + 'PLN' => 'zł', + 'THB' => '฿', + 'MYR' => 'RM', + 'SGD' => 'S$', + 'NZD' => 'NZ$', + 'MXN' => 'Mex$', + 'ZAR' => 'R' + ); + + return isset($symbols[$currency]) ? $symbols[$currency] : $currency; + } + + /** + * Clear cache for specific product + */ + public static function clear_cache($product_id) { + delete_transient('whmcs_product_' . $product_id); + } + + /** + * Clear all cached products + */ + public static function clear_all_cache() { + global $wpdb; + + $wpdb->query( + "DELETE FROM {$wpdb->options} + WHERE option_name LIKE '_transient_whmcs_product_%' + OR option_name LIKE '_transient_timeout_whmcs_product_%'" + ); + } + + /** + * Check if debug mode is enabled + */ + private static function is_debug_mode() { + $settings = self::get_settings(); + return !empty($settings['debug_mode']); + } + + /** + * Log error message + */ + private static function log_error($message) { + if (self::is_debug_mode() && function_exists('error_log')) { + error_log('[WHMCS Pricing] ' . $message); + } + } +} diff --git a/whmcs-pricing-plugin/includes/shortcodes.php b/whmcs-pricing-plugin/includes/shortcodes.php new file mode 100644 index 0000000..3d5df9d --- /dev/null +++ b/whmcs-pricing-plugin/includes/shortcodes.php @@ -0,0 +1,217 @@ + '', + 'billing_cycle' => 'monthly', + 'currency' => '', + 'format' => 'default', + 'show_cycle' => 'true', + 'class' => '', + 'lang' => 'english', + 'language' => '' // Support both lang and language + ), $atts, 'whmcs_price'); + + // Validate product ID + if (empty($atts['product_id'])) { + return $this->render_error(__('Product ID is required', 'whmcs-pricing')); + } + + // Sanitize attributes + $product_id = absint($atts['product_id']); + $billing_cycle = sanitize_text_field($atts['billing_cycle']); + $currency = !empty($atts['currency']) ? sanitize_text_field($atts['currency']) : ''; + $format = sanitize_text_field($atts['format']); + $show_cycle = filter_var($atts['show_cycle'], FILTER_VALIDATE_BOOLEAN); + $custom_class = sanitize_html_class($atts['class']); + + // Support both 'lang' and 'language' parameters + $lang = !empty($atts['language']) ? sanitize_text_field($atts['language']) : sanitize_text_field($atts['lang']); + + // Get price from API + $price_data = WHMCS_Pricing_API::get_price($product_id, array( + 'billing_cycle' => $billing_cycle, + 'currency' => $currency, + 'format' => $format + )); + + // Check for errors + if (is_wp_error($price_data)) { + return $this->render_error($price_data->get_error_message()); + } + + // Render the price + return $this->render_price_html($price_data, $billing_cycle, $product_id, $show_cycle, $custom_class, $lang); + } + + /** + * Render price HTML + * + * @param array $price_data Price data from API + * @param string $billing_cycle Billing cycle + * @param int $product_id Product ID + * @param bool $show_cycle Whether to show billing cycle + * @param string $custom_class Custom CSS class + * @param string $lang Language code + * @return string HTML output + */ + private function render_price_html($price_data, $billing_cycle, $product_id, $show_cycle, $custom_class, $lang = 'english') { + $classes = array('whmcs-price'); + + if (!empty($custom_class)) { + $classes[] = $custom_class; + } + + $cycle_text = $show_cycle ? $this->get_cycle_text($billing_cycle, $lang) : ''; + + $html = sprintf( + '', + esc_attr(implode(' ', $classes)), + esc_attr($product_id), + esc_attr($billing_cycle) + ); + + $html .= sprintf( + '%s', + esc_html($price_data['symbol']) + ); + + $html .= sprintf( + '%s', + esc_html($price_data['formatted']) + ); + + if (!empty($cycle_text)) { + $html .= sprintf( + '%s', + esc_html($cycle_text) + ); + } + + $html .= ''; + + return $html; + } + + /** + * Get billing cycle text + * + * @param string $cycle Billing cycle + * @param string $lang Language code + * @return string Human-readable cycle text + */ + private function get_cycle_text($cycle, $lang = 'english') { + $cycle = strtolower($cycle); + + // Load translations + $translations = self::load_translations(); + + // Check if language exists, fallback to English + if (!isset($translations[$lang])) { + $lang = 'english'; + } + + // Get translated text + if (isset($translations[$lang][$cycle])) { + return ' ' . $translations[$lang][$cycle]; + } + + // Final fallback to English defaults + $defaults = array( + 'monthly' => 'per month', + 'quarterly' => 'per quarter', + 'semiannually' => 'per 6 months', + 'annually' => 'per year', + 'biennially' => 'per 2 years', + 'triennially' => 'per 3 years' + ); + + return isset($defaults[$cycle]) ? ' ' . $defaults[$cycle] : ''; + } + + /** + * Render error message + * + * @param string $message Error message + * @return string HTML output + */ + private function render_error($message) { + // Only show errors to administrators + if (!current_user_can('manage_options')) { + return ''; + } + + return sprintf( + '[WHMCS Pricing Error: %s]', + esc_html($message) + ); + } +} diff --git a/whmcs-pricing-plugin/languages.json b/whmcs-pricing-plugin/languages.json new file mode 100644 index 0000000..5fc00d2 --- /dev/null +++ b/whmcs-pricing-plugin/languages.json @@ -0,0 +1,58 @@ +{ + "english": { + "monthly": "per month", + "quarterly": "per quarter", + "semiannually": "per 6 months", + "annually": "per year", + "biennially": "per 2 years", + "triennially": "per 3 years" + }, + "spanish": { + "monthly": "por mes", + "quarterly": "por trimestre", + "semiannually": "por 6 meses", + "annually": "por año", + "biennially": "por 2 años", + "triennially": "por 3 años" + }, + "portuguese-pt": { + "monthly": "por mês", + "quarterly": "por trimestre", + "semiannually": "por 6 meses", + "annually": "por ano", + "biennially": "por 2 anos", + "triennially": "por 3 anos" + }, + "dutch": { + "monthly": "per maand", + "quarterly": "per kwartaal", + "semiannually": "per 6 maanden", + "annually": "per jaar", + "biennially": "per 2 jaar", + "triennially": "per 3 jaar" + }, + "french": { + "monthly": "par mois", + "quarterly": "par trimestre", + "semiannually": "par 6 mois", + "annually": "par an", + "biennially": "par 2 ans", + "triennially": "par 3 ans" + }, + "german": { + "monthly": "pro Monat", + "quarterly": "pro Quartal", + "semiannually": "pro 6 Monate", + "annually": "pro Jahr", + "biennially": "pro 2 Jahre", + "triennially": "pro 3 Jahre" + }, + "russian": { + "monthly": "в месяц", + "quarterly": "в квартал", + "semiannually": "за 6 месяцев", + "annually": "в год", + "biennially": "за 2 года", + "triennially": "за 3 года" + } +} diff --git a/whmcs-pricing-plugin/whmcs-pricing.php b/whmcs-pricing-plugin/whmcs-pricing.php new file mode 100644 index 0000000..218f92e --- /dev/null +++ b/whmcs-pricing-plugin/whmcs-pricing.php @@ -0,0 +1,164 @@ +load_dependencies(); + $this->init_hooks(); + } + + /** + * Load required files + */ + private function load_dependencies() { + require_once WHMCS_PRICING_PLUGIN_DIR . 'includes/api-handler.php'; + require_once WHMCS_PRICING_PLUGIN_DIR . 'includes/shortcodes.php'; + + if (is_admin()) { + require_once WHMCS_PRICING_PLUGIN_DIR . 'includes/admin-settings.php'; + } + } + + /** + * Initialize hooks + */ + private function init_hooks() { + add_action('plugins_loaded', array($this, 'load_textdomain')); + + // Add settings link on plugins page + add_filter('plugin_action_links_' . WHMCS_PRICING_PLUGIN_BASENAME, array($this, 'add_settings_link')); + + // Make author link open in new tab + add_filter('plugin_row_meta', array($this, 'modify_plugin_row_meta'), 10, 2); + + // Initialize components + if (is_admin()) { + WHMCS_Pricing_Admin::get_instance(); + } + + WHMCS_Pricing_Shortcodes::get_instance(); + } + + /** + * Add settings link on plugins page + */ + public function add_settings_link($links) { + $settings_link = '' . __('Settings', 'whmcs-pricing') . ''; + array_unshift($links, $settings_link); + return $links; + } + + /** + * Modify plugin row meta to open author link in new tab + */ + public function modify_plugin_row_meta($links, $file) { + if ($file === WHMCS_PRICING_PLUGIN_BASENAME) { + // Add target="_blank" to author link + $links = array_map(function($link) { + if (strpos($link, 'arkhost.com') !== false) { + return str_replace(' '', + 'api_identifier' => '', + 'api_secret' => '', + 'cache_duration' => 3600, // 1 hour + 'default_currency' => 'USD', + 'show_decimals' => true, + 'debug_mode' => false + ); + + add_option('whmcs_pricing_settings', $defaults); + } + + /** + * Deactivate plugin + */ + public static function deactivate() { + // Clear all cached data + WHMCS_Pricing_API::clear_all_cache(); + } +} + +/** + * Activation hook + */ +register_activation_hook(__FILE__, array('WHMCS_Pricing_Plugin', 'activate')); + +/** + * Deactivation hook + */ +register_deactivation_hook(__FILE__, array('WHMCS_Pricing_Plugin', 'deactivate')); + +/** + * Initialize plugin + */ +function whmcs_pricing_init() { + return WHMCS_Pricing_Plugin::get_instance(); +} + +// Start the plugin +whmcs_pricing_init();