commit 271960206cd5be5273e51d65a8a529f4452ec5e8 Author: Yuri Date: Sun Nov 16 21:47:01 2025 +0100 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..09cb51b --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# WHMCS Pricing + +Display WHMCS product prices on WordPress using shortcodes. Lightweight, cached, works with Elementor. + +## Installation + +1. Upload ZIP via `Plugins > Add New > Upload Plugin` +2. Activate +3. Go to `Settings > WHMCS Pricing` +4. Enter your WHMCS credentials + +## WHMCS Setup + +**Step 1: Allow API Access by IP** + +1. Go to `General Settings > Security` tab in WHMCS +2. Find **API IP Access Restriction** +3. Add your WordPress server IP address +4. Save changes + +**Step 2: Generate API Credentials** + +1. Go to `System Settings > API Credentials` +2. Click `Generate New API Credential` +3. Set Access Type to `Allowed` +4. Copy Identifier and Secret + +Enter these in WordPress plugin settings along with your WHMCS URL. + +**Security Note:** Without adding your WordPress server IP to API IP Access Restriction, the API calls will be blocked by WHMCS. + +## Usage + +Basic shortcode: +``` +[whmcs_price product_id="123" billing_cycle="annually"] +``` + +### Parameters + +- `product_id` - WHMCS product ID (required) +- `billing_cycle` - monthly, quarterly, semiannually, annually, biennially, triennially (default: monthly) +- `currency` - EUR, USD, GBP, etc. (uses plugin default if not specified) +- `format` - `default` or `no_decimals` +- `show_cycle` - `true` or `false` (default: true) +- `lang` - Language: `english`, `spanish`, `portuguese-pt`, `dutch`, `french`, `german`, `russian` (default: english) +- `class` - Custom CSS class + +### Examples + +``` +[whmcs_price product_id="5" billing_cycle="annually"] +Output: €24.00 per year + +[whmcs_price product_id="5" billing_cycle="monthly" format="no_decimals"] +Output: €3 per month + +[whmcs_price product_id="5" currency="USD" show_cycle="false"] +Output: $3.50 + +[whmcs_price product_id="5" billing_cycle="annually" lang="spanish"] +Output: €24.00 por año + +[whmcs_price product_id="5" billing_cycle="monthly" lang="dutch"] +Output: €3.00 per maand +``` + +### Translations + +Billing cycle text is translated via `languages.json`. Supported languages: +- `english` +- `spanish` +- `portuguese-pt` +- `dutch` +- `french` +- `german` +- `russian` + +Add more languages by editing `languages.json` in the plugin folder (uses WHMCS language naming). + +```json +{ + "italian": { + "monthly": "al mese", + "annually": "all'anno" + } +} +``` + +### Find Product IDs + +In WHMCS admin: +1. `System Settings > Products/Services` +2. Click a product +3. ID is in URL: `?id=123` + +## HTML Output + +```html + + + 24.00 + per year + +``` + +Style it with CSS: + +```css +.whmcs-price { font-weight: bold; } +.price-amount { font-size: 24px; } +``` + +## Important Notes + +**WHMCS uses `-1.00` for unavailable billing cycles.** If a product doesn't have monthly pricing in WHMCS, don't use `billing_cycle="monthly"` in the shortcode. Check your WHMCS product configuration first. + +**Caching:** Prices are cached for 1 hour by default. Change this in plugin settings. Click "Clear All Cache" after updating prices in WHMCS. + +**Errors:** Only administrators see error messages. Regular visitors see nothing if a price can't be loaded. + +## Settings + +- **WHMCS URL** - Your WHMCS installation URL (https://billing.yourdomain.com) +- **API Identifier** - From WHMCS API credentials +- **API Secret** - From WHMCS API credentials +- **Default Currency** - Fallback currency (EUR, USD, etc.) +- **Show Decimals** - Toggle decimal places globally +- **Cache Duration** - How long to cache API responses (seconds) +- **Debug Mode** - Enable logging, disable cache + +## Troubleshooting + +**Price shows as -1.00:** +- That billing cycle isn't available for this product in WHMCS +- Use a different `billing_cycle` parameter + +**No price displays:** +- Check product ID exists in WHMCS +- Verify API credentials are correct +- Enable Debug Mode and check `/wp-content/debug.log` +- Clear plugin cache + +**Wrong price:** +- Clear cache (prices are cached) +- Check currency matches WHMCS configuration +- Verify billing cycle is available + +## Requirements + +- WordPress 5.0+ +- PHP 7.4+ +- WHMCS 8.x with API access +- SSL recommended + +## Support + +**By:** ArkHost +**Sponsored by:** Duplika.com +**License:** GPL v2+ diff --git a/whmcs-pricing-plugin/.gitignore b/whmcs-pricing-plugin/.gitignore new file mode 100644 index 0000000..79a676a --- /dev/null +++ b/whmcs-pricing-plugin/.gitignore @@ -0,0 +1,68 @@ +# WordPress +.htaccess +wp-config.php +wp-content/uploads/ +wp-content/blogs.dir/ +wp-content/upgrade/ +wp-content/backup-db/ +wp-content/advanced-cache.php +wp-content/wp-cache-config.php +wp-content/cache/ +wp-content/cache/supercache/ + +# Plugin Development +node_modules/ +vendor/ +composer.lock +package-lock.json +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace +.DS_Store +Thumbs.db + +# Testing +tests/ +phpunit.xml +.phpunit.result.cache + +# Build +dist/ +build/ +*.zip +!whmcs-pricing-plugin.zip + +# Environment +.env +.env.local +.env.*.local + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +*~ + +# Temporary +*.tmp +*.temp +*.bak +*.swp +*.swo +*~ + +# Logs +*.log +error_log +debug.log diff --git a/whmcs-pricing-plugin/assets/css/admin.css b/whmcs-pricing-plugin/assets/css/admin.css new file mode 100644 index 0000000..c7bf927 --- /dev/null +++ b/whmcs-pricing-plugin/assets/css/admin.css @@ -0,0 +1,147 @@ +/** + * WHMCS Pricing Admin Styles + */ + +.whmcs-pricing-admin { + max-width: 1200px; +} + +.whmcs-pricing-container { + display: flex; + gap: 30px; + margin-top: 20px; +} + +.whmcs-pricing-main { + flex: 1; + min-width: 0; +} + +.whmcs-pricing-sidebar { + width: 350px; + flex-shrink: 0; +} + +.whmcs-pricing-box { + background: #fff; + border: 1px solid #ccd0d4; + border-radius: 4px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.whmcs-pricing-box h3 { + margin-top: 0; + margin-bottom: 15px; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid #eee; + padding-bottom: 10px; +} + +.whmcs-pricing-box h4 { + margin-top: 20px; + margin-bottom: 8px; + font-size: 13px; + font-weight: 600; + color: #555; +} + +.whmcs-pricing-box h4:first-of-type { + margin-top: 15px; +} + +.whmcs-pricing-box code { + display: block; + background: #f6f7f7; + border: 1px solid #ddd; + border-radius: 3px; + padding: 10px; + margin: 8px 0 15px 0; + font-size: 12px; + font-family: 'Courier New', monospace; + word-wrap: break-word; + color: #c92c2c; +} + +.whmcs-pricing-box ul { + margin: 10px 0; + padding-left: 20px; +} + +.whmcs-pricing-box ul li { + margin: 5px 0; + font-size: 13px; + color: #555; +} + +.whmcs-pricing-box p { + margin: 10px 0; + line-height: 1.6; +} + +.whmcs-pricing-box p strong { + color: #1d2327; +} + +/* Form styles */ +.whmcs-pricing-admin .form-table th { + width: 200px; +} + +.whmcs-pricing-admin input[type="text"], +.whmcs-pricing-admin input[type="password"], +.whmcs-pricing-admin input[type="number"], +.whmcs-pricing-admin select { + width: 100%; + max-width: 500px; +} + +.whmcs-pricing-admin .description { + color: #646970; + font-size: 13px; + margin-top: 8px; +} + +/* Settings sections */ +.whmcs-pricing-admin h2 { + margin-top: 30px; + padding-bottom: 10px; + border-bottom: 2px solid #2271b1; + color: #1d2327; +} + +/* Buttons */ +.whmcs-pricing-admin .submit { + padding-top: 0; + margin-bottom: 20px; +} + +/* Responsive */ +@media screen and (max-width: 960px) { + .whmcs-pricing-container { + flex-direction: column; + } + + .whmcs-pricing-sidebar { + width: 100%; + } + + .whmcs-pricing-admin input[type="text"], + .whmcs-pricing-admin input[type="password"], + .whmcs-pricing-admin input[type="number"], + .whmcs-pricing-admin select { + max-width: 100%; + } +} + +/* Success/Error messages */ +.whmcs-pricing-admin .notice { + margin: 20px 0; +} + +/* Help text */ +.whmcs-pricing-admin .form-table td p:first-child { + margin-top: 0; +} diff --git a/whmcs-pricing-plugin/includes/admin-settings.php b/whmcs-pricing-plugin/includes/admin-settings.php new file mode 100644 index 0000000..39390f5 --- /dev/null +++ b/whmcs-pricing-plugin/includes/admin-settings.php @@ -0,0 +1,551 @@ + 'api_url', + 'placeholder' => 'https://your-whmcs-domain.com', + 'description' => __('Your WHMCS installation URL (without /includes/api.php)', 'whmcs-pricing') + ) + ); + + add_settings_field( + 'api_identifier', + __('API Identifier', 'whmcs-pricing'), + array($this, 'render_text_field'), + 'whmcs-pricing', + 'whmcs_pricing_api_section', + array( + 'name' => 'api_identifier', + 'description' => __('Your WHMCS API identifier', 'whmcs-pricing') + ) + ); + + add_settings_field( + 'api_secret', + __('API Secret', 'whmcs-pricing'), + array($this, 'render_password_field'), + 'whmcs-pricing', + 'whmcs_pricing_api_section', + array( + 'name' => 'api_secret', + 'description' => __('Your WHMCS API secret', 'whmcs-pricing') + ) + ); + + // Display Settings Section + add_settings_section( + 'whmcs_pricing_display_section', + __('Display Settings', 'whmcs-pricing'), + array($this, 'render_display_section'), + 'whmcs-pricing' + ); + + add_settings_field( + 'default_currency', + __('Default Currency', 'whmcs-pricing'), + array($this, 'render_currency_field'), + 'whmcs-pricing', + 'whmcs_pricing_display_section', + array( + 'name' => 'default_currency', + 'description' => __('Default currency for pricing display', 'whmcs-pricing') + ) + ); + + add_settings_field( + 'show_decimals', + __('Show Decimals', 'whmcs-pricing'), + array($this, 'render_checkbox_field'), + 'whmcs-pricing', + 'whmcs_pricing_display_section', + array( + 'name' => 'show_decimals', + 'label' => __('Display prices with decimal places (e.g., €67.00 instead of €67)', 'whmcs-pricing') + ) + ); + + // Performance Settings Section + add_settings_section( + 'whmcs_pricing_performance_section', + __('Performance Settings', 'whmcs-pricing'), + array($this, 'render_performance_section'), + 'whmcs-pricing' + ); + + add_settings_field( + 'cache_duration', + __('Cache Duration', 'whmcs-pricing'), + array($this, 'render_number_field'), + 'whmcs-pricing', + 'whmcs_pricing_performance_section', + array( + 'name' => 'cache_duration', + 'min' => 300, + 'max' => 86400, + 'step' => 300, + 'description' => __('How long to cache API responses (in seconds). Default: 3600 (1 hour)', 'whmcs-pricing') + ) + ); + + // Advanced Settings Section + add_settings_section( + 'whmcs_pricing_advanced_section', + __('Advanced Settings', 'whmcs-pricing'), + array($this, 'render_advanced_section'), + 'whmcs-pricing' + ); + + add_settings_field( + 'debug_mode', + __('Debug Mode', 'whmcs-pricing'), + array($this, 'render_checkbox_field'), + 'whmcs-pricing', + 'whmcs_pricing_advanced_section', + array( + 'name' => 'debug_mode', + 'label' => __('Enable debug logging and disable cache', 'whmcs-pricing') + ) + ); + } + + /** + * Render settings page + */ + public function render_settings_page() { + if (!current_user_can('manage_options')) { + return; + } + + // Show success message + if (isset($_GET['settings-updated'])) { + add_settings_error( + 'whmcs_pricing_messages', + 'whmcs_pricing_message', + __('Settings saved successfully.', 'whmcs-pricing'), + 'updated' + ); + } + + if (isset($_GET['cache-cleared'])) { + add_settings_error( + 'whmcs_pricing_messages', + 'whmcs_pricing_message', + __('Cache cleared successfully.', 'whmcs-pricing'), + 'updated' + ); + } + + settings_errors('whmcs_pricing_messages'); + ?> +
+

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

+ +

+ +
+

+ +

+ [whmcs_price product_id="123"] + +

+ [whmcs_price product_id="123" billing_cycle="monthly"] + +

+ [whmcs_price product_id="123" currency="EUR"] + +

+ [whmcs_price product_id="123" format="no_decimals"] + +

+ [whmcs_price product_id="123" lang="dutch"] + +

+
    +
  • monthly
  • +
  • quarterly
  • +
  • semiannually
  • +
  • annually
  • +
  • biennially
  • +
  • triennially
  • +
+
+
+ +
+

+ +

+

+ +

+ +
+ +
+

+

+

ArkHost

+

Duplika

+
+
+
+
+ + + ' . esc_html__('Configure your WHMCS API credentials. You can generate API credentials in WHMCS Admin > System Settings > API Credentials.', 'whmcs-pricing') . '

'; + } + + public function render_display_section() { + echo '

' . 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();