From a8f851a7efa324e7676fff8d0e4d27f816de6feb Mon Sep 17 00:00:00 2001 From: Aljaz Ceru Date: Sat, 17 May 2025 13:55:17 +0200 Subject: [PATCH] initial commit --- assets/css/breez-woocommerce.css | 91 +++ assets/js/blocks.js | 147 +++++ assets/js/breez-woocommerce.js | 107 ++++ breez-woocommerce.php | 699 +++++++++++++++++++++++ class-wc-gateway-breez.php | 692 ++++++++++++++++++++++ composer.json | 19 + includes/admin/breez-settings.php | 107 ++++ includes/block/breez-payment.asset.php | 10 + includes/block/breez-payment.js | 86 +++ includes/block/breez-widget.asset.php | 8 + includes/block/breez-widget.js | 8 + includes/class-breez-api-client.php | 464 +++++++++++++++ includes/class-breez-blocks-support.php | 224 ++++++++ includes/class-breez-db-manager.php | 261 +++++++++ includes/class-breez-logger.php | 183 ++++++ includes/class-breez-payment-handler.php | 389 +++++++++++++ includes/class-breez-webhook-handler.php | 125 ++++ templates/admin-settings.php | 50 ++ templates/payment-instructions.php | 463 +++++++++++++++ 19 files changed, 4133 insertions(+) create mode 100644 assets/css/breez-woocommerce.css create mode 100644 assets/js/blocks.js create mode 100644 assets/js/breez-woocommerce.js create mode 100755 breez-woocommerce.php create mode 100644 class-wc-gateway-breez.php create mode 100644 composer.json create mode 100644 includes/admin/breez-settings.php create mode 100644 includes/block/breez-payment.asset.php create mode 100644 includes/block/breez-payment.js create mode 100644 includes/block/breez-widget.asset.php create mode 100644 includes/block/breez-widget.js create mode 100644 includes/class-breez-api-client.php create mode 100644 includes/class-breez-blocks-support.php create mode 100644 includes/class-breez-db-manager.php create mode 100644 includes/class-breez-logger.php create mode 100644 includes/class-breez-payment-handler.php create mode 100644 includes/class-breez-webhook-handler.php create mode 100644 templates/admin-settings.php create mode 100644 templates/payment-instructions.php diff --git a/assets/css/breez-woocommerce.css b/assets/css/breez-woocommerce.css new file mode 100644 index 0000000..92ce20e --- /dev/null +++ b/assets/css/breez-woocommerce.css @@ -0,0 +1,91 @@ +/** + * Breez WooCommerce Styles + */ + +.breez-payment-box { + max-width: 600px; + margin: 0 auto; + padding: 20px; + background: #f8f8f8; + border-radius: 5px; + text-align: center; +} + +.breez-payment-status { + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.breez-payment-completed { + background: #d4edda; + color: #155724; +} + +.breez-payment-failed { + background: #f8d7da; + color: #721c24; +} + +.breez-payment-countdown { + font-size: 18px; + margin-bottom: 20px; +} + +.breez-countdown { + font-weight: bold; +} + +.breez-payment-qr { + margin-bottom: 20px; +} + +.breez-payment-qr img { + max-width: 250px; + height: auto; +} + +.breez-invoice-container { + position: relative; + margin-bottom: 20px; +} + +.breez-invoice-text { + width: 100%; + padding: 10px; + font-family: monospace; + font-size: 14px; + resize: none; +} + +.breez-copy-button { + position: absolute; + right: 10px; + top: 10px; + padding: 5px 10px; + background: #0073aa; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; +} + +.breez-copy-button:hover { + background: #005177; +} + +.breez-payment-info { + margin-bottom: 20px; +} + +.breez-payment-refresh { + margin-top: 20px; +} + +.breez-payment-expired { + background: #f8d7da; + color: #721c24; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} diff --git a/assets/js/blocks.js b/assets/js/blocks.js new file mode 100644 index 0000000..ed35d28 --- /dev/null +++ b/assets/js/blocks.js @@ -0,0 +1,147 @@ +/** + * Breez Payment Method for WooCommerce Blocks + * Compatible with WooCommerce Blocks version 9.x + */ +(function() { + // Safe console logging + var log = function(msg) { + if (window.console && window.console.log) { + console.log('[Breez] ' + msg); + } + }; + + // Register payment method function + function registerBreezPaymentMethod(registerFn) { + try { + // Create React element for content + var Content = function() { + return window.wp.element.createElement( + 'div', + null, + window.breezSettings?.description || 'Pay with Bitcoin via Lightning Network or on-chain transaction.' + ); + }; + + registerFn({ + name: 'breez', + label: window.breezSettings?.title || 'Breez Nodeless Payments', + content: window.wp.element.createElement(Content, null), + edit: window.wp.element.createElement(Content, null), + canMakePayment: function() { return true; }, + ariaLabel: window.breezSettings?.title || 'Breez Nodeless Payments', + supports: { + features: window.breezSettings?.supports || ['products'] + }, + paymentMethodId: 'breez', + billing: { + required: true + }, + // Add data to be sent to the server + getData: function() { + log('Getting payment data'); + return { + payment_method: 'breez', + payment_data: { + breez_payment_method: 'LIGHTNING' + } + }; + }, + // Add payment processing + onSubmit: function(data) { + log('Processing payment submission'); + return { + type: 'success', + meta: { + paymentMethodData: { + payment_method: 'breez', + payment_data: { + breez_payment_method: 'LIGHTNING' + } + } + } + }; + }, + // Add payment processing status + onPaymentProcessing: function() { + log('Payment processing started'); + return { + type: 'success', + meta: { + paymentMethodData: { + payment_method: 'breez', + payment_data: { + breez_payment_method: 'LIGHTNING' + } + } + } + }; + } + }); + log('Payment method registered successfully'); + } catch (error) { + log('Error registering payment method: ' + error.message); + } + } + + // Wait for DOM content to be loaded + document.addEventListener('DOMContentLoaded', function() { + // Only run on checkout page + if (!document.body.classList.contains('woocommerce-checkout')) { + return; + } + + log('Initializing Breez payment method for WooCommerce Blocks 9.x'); + + // Check if blocks object exists in newer location + if (window.wc && window.wc.blocks && window.wc.blocks.registry && + typeof window.wc.blocks.registry.registerPaymentMethod === 'function') { + + log('Found WC Blocks registry in wc.blocks.registry'); + registerBreezPaymentMethod(window.wc.blocks.registry.registerPaymentMethod); + + } + // Try fallback locations + else if (window.wc && window.wc.wcBlocksRegistry && + typeof window.wc.wcBlocksRegistry.registerPaymentMethod === 'function') { + + log('Found WC Blocks registry in wc.wcBlocksRegistry'); + registerBreezPaymentMethod(window.wc.wcBlocksRegistry.registerPaymentMethod); + + } else { + // Log available WooCommerce structure to help debug + var wcPaths = []; + + if (window.wc) { + wcPaths.push('wc'); + + if (window.wc.blocks) { + wcPaths.push('wc.blocks'); + + if (window.wc.blocks.registry) { + wcPaths.push('wc.blocks.registry'); + if (typeof window.wc.blocks.registry.registerPaymentMethod === 'function') { + wcPaths.push('wc.blocks.registry.registerPaymentMethod (function)'); + } + } + + if (window.wc.blocks.payment) { + wcPaths.push('wc.blocks.payment'); + } + } + + if (window.wc.wcBlocksRegistry) { + wcPaths.push('wc.wcBlocksRegistry'); + if (typeof window.wc.wcBlocksRegistry.registerPaymentMethod === 'function') { + wcPaths.push('wc.wcBlocksRegistry.registerPaymentMethod (function)'); + } + } + } + + if (wcPaths.length > 0) { + log('WooCommerce paths found: ' + wcPaths.join(', ')); + } else { + log('No WooCommerce Blocks registry found. Payment method registration not possible.'); + } + } + }); +})(); \ No newline at end of file diff --git a/assets/js/breez-woocommerce.js b/assets/js/breez-woocommerce.js new file mode 100644 index 0000000..0d20cb1 --- /dev/null +++ b/assets/js/breez-woocommerce.js @@ -0,0 +1,107 @@ +/** + * Breez WooCommerce JavaScript + */ + +document.addEventListener('DOMContentLoaded', function() { + // Countdown functionality + var countdownEl = document.querySelector('.breez-countdown'); + var countdownContainer = document.querySelector('.breez-payment-countdown'); + + if (countdownEl && countdownContainer) { + var expiryTime = parseInt(countdownContainer.dataset.expiry, 10); + + if (expiryTime) { + var countdownInterval = setInterval(function() { + var now = Math.floor(Date.now() / 1000); + var timeLeft = expiryTime - now; + + if (timeLeft <= 0) { + clearInterval(countdownInterval); + countdownContainer.innerHTML = '

Payment time expired.

'; + document.querySelector('.breez-payment-qr').style.opacity = '0.3'; + + // Reload page to show expired message + setTimeout(function() { + window.location.reload(); + }, 3000); + + return; + } + + var minutes = Math.floor(timeLeft / 60); + var seconds = timeLeft % 60; + countdownEl.textContent = (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds; + }, 1000); + } + } + + // Copy invoice button functionality + var copyButton = document.querySelector('.breez-copy-button'); + if (copyButton) { + copyButton.addEventListener('click', function() { + var textToCopy = this.dataset.clipboardText; + var textarea = document.createElement('textarea'); + textarea.value = textToCopy; + document.body.appendChild(textarea); + textarea.select(); + + try { + document.execCommand('copy'); + this.textContent = 'Copied!'; + setTimeout(function() { + copyButton.textContent = 'Copy'; + }, 2000); + } catch (err) { + console.error('Failed to copy: ', err); + } + + document.body.removeChild(textarea); + }); + } + + // Auto-refresh payment status + function checkPaymentStatus() { + var orderStatusCheck = document.querySelector('.breez-payment-box'); + if (orderStatusCheck && !document.querySelector('.breez-payment-completed') && !document.querySelector('.breez-payment-failed') && !document.querySelector('.breez-payment-expired')) { + // Get current URL + var currentUrl = window.location.href; + + // Append a random parameter to force a fresh response + var refreshUrl = currentUrl + (currentUrl.indexOf('?') > -1 ? '&' : '?') + '_=' + Math.random(); + + // Make AJAX request to check status + var xhr = new XMLHttpRequest(); + xhr.open('GET', refreshUrl, true); + xhr.onload = function() { + if (xhr.status === 200) { + // Parse HTML response + var parser = new DOMParser(); + var doc = parser.parseFromString(xhr.responseText, 'text/html'); + + // Find payment box in the response + var newPaymentBox = doc.querySelector('.breez-payment-box'); + + if (newPaymentBox) { + // Check if payment is now completed + if (newPaymentBox.querySelector('.breez-payment-completed')) { + orderStatusCheck.innerHTML = newPaymentBox.innerHTML; + clearInterval(statusInterval); + } + // Check if payment has failed + else if (newPaymentBox.querySelector('.breez-payment-failed')) { + orderStatusCheck.innerHTML = newPaymentBox.innerHTML; + clearInterval(statusInterval); + } + } + } + }; + xhr.send(); + } else { + // If payment is already completed or failed, stop checking + clearInterval(statusInterval); + } + } + + // Check every 10 seconds + var statusInterval = setInterval(checkPaymentStatus, 10000); +}); diff --git a/breez-woocommerce.php b/breez-woocommerce.php new file mode 100755 index 0000000..57cbea0 --- /dev/null +++ b/breez-woocommerce.php @@ -0,0 +1,699 @@ + 'install-plugin', + 'plugin' => 'woocommerce', + ], + admin_url('update.php') + ), + 'install-plugin_woocommerce' + ); + + printf( + '

%s

', + wp_kses_post( + sprintf( + '%1$sBreez Nodeless Payments%2$s requires the %3$sWooCommerce plugin%4$s to be active. Please %5$sinstall & activate WooCommerce »%6$s', + '', + '', + '', + '', + '', + '' + ) + ) + ); +} + +/** + * Check if WooCommerce is active + */ +function breez_wc_is_woocommerce_active() { + $active_plugins = (array) get_option('active_plugins', array()); + + if (is_multisite()) { + $active_plugins = array_merge($active_plugins, get_site_option('active_sitewide_plugins', array())); + } + + return in_array('woocommerce/woocommerce.php', $active_plugins) || array_key_exists('woocommerce/woocommerce.php', $active_plugins); +} + +/** + * Initialize the plugin + */ +function breez_wc_init() { + // Check if WooCommerce is fully loaded + if (!class_exists('WC_Payment_Gateway')) { + return; + } + + try { + // Initialize logger first + require_once BREEZ_WC_PLUGIN_DIR . 'includes/class-breez-logger.php'; + $logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); + $logger->log('Initializing Breez WooCommerce plugin', 'debug'); + + // Load plugin textdomain + load_plugin_textdomain('breez-woocommerce', false, basename(dirname(__FILE__)) . '/languages'); + + // Include required files in specific order + $required_files = array( + 'includes/class-breez-api-client.php', + 'includes/class-breez-db-manager.php', + 'includes/class-breez-logger.php', + 'includes/class-breez-webhook-handler.php', + 'includes/class-breez-payment-handler.php', + 'class-wc-gateway-breez.php' + ); + + foreach ($required_files as $file) { + $full_path = BREEZ_WC_PLUGIN_DIR . $file; + if (!file_exists($full_path)) { + throw new Exception("Required file not found: $file"); + } + require_once $full_path; + } + + // Verify WooCommerce settings + if (!get_option('woocommerce_currency')) { + throw new Exception('WooCommerce currency not configured'); + } + + // Register payment gateway + add_filter('woocommerce_payment_gateways', 'breez_wc_add_gateway'); + + // Add settings link on plugin page + add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'breez_wc_settings_link'); + + // Register webhook handler + add_action('rest_api_init', 'breez_wc_register_webhook_endpoint'); + + // Schedule payment status check + if (!wp_next_scheduled('breez_wc_check_pending_payments')) { + wp_schedule_event(time(), 'five_minutes', 'breez_wc_check_pending_payments'); + $logger->log('Scheduled payment status check', 'debug'); + } + + // Check API credentials + $breez_settings = get_option('woocommerce_breez_settings', array()); + $api_url = isset($breez_settings['api_url']) ? $breez_settings['api_url'] : ''; + $api_key = isset($breez_settings['api_key']) ? $breez_settings['api_key'] : ''; + + if (!$api_url || !$api_key) { + $logger->log('API credentials not configured', 'warning'); + $logger->log('Settings: ' . print_r($breez_settings, true), 'debug'); + add_action('admin_notices', function() { + echo '

' . + __('Breez Nodeless Payments requires API credentials to be configured.', 'breez-woocommerce') . + ' ' . + __('Configure Now', 'breez-woocommerce') . '

'; + }); + } else { + // Test API connection + try { + $client = new Breez_API_Client($api_url, $api_key); + if (!$client->check_health()) { + throw new Exception('API health check failed'); + } + $logger->log('API connection verified', 'debug'); + } catch (Exception $e) { + $logger->log('API connection test failed: ' . $e->getMessage(), 'error'); + add_action('admin_notices', function() { + echo '

' . + __('Breez Nodeless Payments could not connect to the API. Please check your settings.', 'breez-woocommerce') . + '

'; + }); + } + } + + $logger->log('Plugin initialization completed', 'debug'); + + } catch (Exception $e) { + if (isset($logger)) { + $logger->log('Plugin initialization failed: ' . $e->getMessage(), 'error'); + } + add_action('admin_notices', function() use ($e) { + echo '

' . + __('Breez Nodeless Payments initialization failed: ', 'breez-woocommerce') . + esc_html($e->getMessage()) . '

'; + }); + } +} + +/** + * Add the Breez gateway to WooCommerce + */ +function breez_wc_add_gateway($gateways) { + if (class_exists('Breez_Logger')) { + $logger = new Breez_Logger(true); + $logger->log("Adding Breez gateway to WooCommerce", 'debug'); + } + + if (class_exists('WC_Gateway_Breez')) { + $gateways[] = 'WC_Gateway_Breez'; + if (class_exists('Breez_Logger')) { + $logger->log("Breez gateway class added successfully", 'debug'); + } + return $gateways; + } + + if (class_exists('Breez_Logger')) { + $logger->log("WC_Gateway_Breez class not found", 'error'); + } + return $gateways; +} + +/** + * Add settings link on plugin page + */ +function breez_wc_settings_link($links) { + $settings_link = '' . __('Settings', 'breez-woocommerce') . ''; + array_unshift($links, $settings_link); + return $links; +} + +/** + * Register webhook endpoint + */ +function breez_wc_register_webhook_endpoint() { + register_rest_route('breez-wc/v1', '/webhook', array( + 'methods' => 'POST', + 'callback' => array('Breez_Webhook_Handler', 'process_webhook'), + 'permission_callback' => array('Breez_Webhook_Handler', 'validate_webhook'), + )); + + // Register payment status check endpoint + register_rest_route('breez-wc/v1', '/check-payment-status', array( + 'methods' => ['GET', 'POST'], + 'callback' => 'breez_wc_check_payment_status_endpoint', + 'permission_callback' => '__return_true', + 'args' => array( + 'order_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ), + )); + + // Register direct payment status check endpoint with invoice ID + register_rest_route('breez-wc/v1', '/check-payment-status/(?P[\w-]+)', array( + 'methods' => 'GET', + 'callback' => 'breez_wc_check_payment_status_by_invoice_endpoint', + 'permission_callback' => '__return_true', + )); +} + +/** + * Direct payment status check endpoint handler (with invoice ID in URL) + */ +function breez_wc_check_payment_status_by_invoice_endpoint($request) { + try { + // Initialize logger + $logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); + $logger->log('Direct payment status check endpoint called', 'debug'); + + $invoice_id = $request->get_param('invoice_id'); + $logger->log("Checking payment status for invoice ID: $invoice_id", 'debug'); + + // Initialize API client - directly fetch gateway settings + $gateway_settings = get_option('woocommerce_breez_settings'); + $api_url = isset($gateway_settings['api_url']) ? $gateway_settings['api_url'] : ''; + $api_key = isset($gateway_settings['api_key']) ? $gateway_settings['api_key'] : ''; + + $logger->log("API settings: URL=" . (!empty($api_url) ? 'Set' : 'Missing') . + ", Key=" . (!empty($api_key) ? 'Set' : 'Missing'), 'debug'); + + if (empty($api_url) || empty($api_key)) { + $logger->log("API credentials not configured", 'error'); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Payment gateway not properly configured' + ), 500); + } + + $client = new Breez_API_Client($api_url, $api_key); + $payment_status = $client->check_payment_status($invoice_id); + $logger->log("Payment status for invoice ID $invoice_id: " . print_r($payment_status, true), 'debug'); + + return new WP_REST_Response(array( + 'success' => true, + 'status' => $payment_status['status'], + 'data' => $payment_status + ), 200); + + } catch (Exception $e) { + if (isset($logger)) { + $logger->log('Payment status check failed: ' . $e->getMessage(), 'error'); + $logger->log('Stack trace: ' . $e->getTraceAsString(), 'debug'); + } + + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Internal server error: ' . $e->getMessage() + ), 500); + } +} + +/** + * Payment status check endpoint handler + */ +function breez_wc_check_payment_status_endpoint($request) { + try { + // Initialize logger + $logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); + $logger->log('Payment status check endpoint called', 'debug'); + + $order_id = $request->get_param('order_id'); + $logger->log("Checking payment status for order #$order_id", 'debug'); + + $order = wc_get_order($order_id); + if (!$order) { + $logger->log("Order #$order_id not found", 'error'); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Order not found' + ), 404); + } + + // Only allow checking status of Breez payments + if ($order->get_payment_method() !== 'breez') { + $logger->log("Invalid payment method for order #$order_id: " . $order->get_payment_method(), 'error'); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Invalid payment method' + ), 400); + } + + // If this is a POST request, update the order based on the payment data + if ($request->get_method() === 'POST') { + $payment_data = $request->get_json_params(); + $logger->log("Received payment data for order #$order_id: " . print_r($payment_data, true), 'debug'); + + if ($payment_data['status'] === 'SUCCEEDED') { + if ($order->get_status() === 'pending') { + // Add payment details to order meta + $order->update_meta_data('_breez_payment_amount_sat', $payment_data['amount_sat']); + $order->update_meta_data('_breez_payment_fees_sat', $payment_data['fees_sat']); + $order->update_meta_data('_breez_payment_time', $payment_data['payment_time']); + $order->update_meta_data('_breez_payment_hash', $payment_data['payment_hash']); + + // Complete the order + $order->payment_complete($payment_data['payment_hash']); + $order->add_order_note(sprintf( + __('Payment confirmed. Amount: %d sats, Fees: %d sats, Hash: %s', 'breez-woocommerce'), + $payment_data['amount_sat'], + $payment_data['fees_sat'], + $payment_data['payment_hash'] + )); + $order->save(); + + $logger->log("Order #$order_id marked as complete", 'info'); + } + + return new WP_REST_Response(array( + 'success' => true, + 'status' => 'completed' + ), 200); + } elseif ($payment_data['status'] === 'FAILED') { + if ($order->get_status() === 'pending') { + $order->update_status('failed', $payment_data['error'] ?? __('Payment failed', 'breez-woocommerce')); + $order->save(); + + $logger->log("Order #$order_id marked as failed: " . ($payment_data['error'] ?? 'No error message'), 'info'); + } + + return new WP_REST_Response(array( + 'success' => true, + 'status' => 'failed' + ), 200); + } + } + + // For GET requests, check payment status via API + $invoice_id = $order->get_meta('_breez_invoice_id'); + if (!$invoice_id) { + $logger->log("No payment invoice found for order #$order_id", 'error'); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'No payment invoice found' + ), 404); + } + + // Initialize API client - directly fetch gateway settings + $gateway_settings = get_option('woocommerce_breez_settings'); + $api_url = isset($gateway_settings['api_url']) ? $gateway_settings['api_url'] : ''; + $api_key = isset($gateway_settings['api_key']) ? $gateway_settings['api_key'] : ''; + + $logger->log("API settings: URL=" . (!empty($api_url) ? 'Set' : 'Missing') . + ", Key=" . (!empty($api_key) ? 'Set' : 'Missing'), 'debug'); + + if (empty($api_url) || empty($api_key)) { + $logger->log("API credentials not configured", 'error'); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Payment gateway not properly configured' + ), 500); + } + + $client = new Breez_API_Client($api_url, $api_key); + $payment_status = $client->check_payment_status($invoice_id); + $logger->log("Payment status for order #$order_id: " . print_r($payment_status, true), 'debug'); + + return new WP_REST_Response(array( + 'success' => true, + 'status' => $payment_status['status'], + 'data' => $payment_status + ), 200); + + } catch (Exception $e) { + if (isset($logger)) { + $logger->log('Payment status check failed: ' . $e->getMessage(), 'error'); + $logger->log('Stack trace: ' . $e->getTraceAsString(), 'debug'); + } + + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Internal server error: ' . $e->getMessage() + ), 500); + } +} + +/** + * Check pending payments (runs via cron) + */ +function breez_wc_check_pending_payments() { + $payment_handler = new Breez_Payment_Handler(); + $payment_handler->check_pending_payments(); +} + +/** + * Plugin activation hook + */ +function breez_wc_activate() { + if (!breez_wc_is_woocommerce_active()) { + deactivate_plugins(plugin_basename(__FILE__)); + wp_die(__('Breez Nodeless Payments requires WooCommerce to be installed and active.', 'breez-woocommerce')); + } + + // Include required files before using them + require_once BREEZ_WC_PLUGIN_DIR . 'includes/class-breez-logger.php'; + require_once BREEZ_WC_PLUGIN_DIR . 'includes/class-breez-db-manager.php'; + + // Create required directories if they don't exist + $directories = array( + BREEZ_WC_PLUGIN_DIR . 'includes/admin', + BREEZ_WC_PLUGIN_DIR . 'includes/block', + BREEZ_WC_PLUGIN_DIR . 'assets/js', + BREEZ_WC_PLUGIN_DIR . 'assets/css', + ); + + foreach ($directories as $directory) { + if (!file_exists($directory)) { + wp_mkdir_p($directory); + } + } + + // Create database tables + $db_manager = new Breez_DB_Manager(); + $db_manager->install_tables(); + + // Schedule payment status check + if (!wp_next_scheduled('breez_wc_check_pending_payments')) { + wp_schedule_event(time(), 'five_minutes', 'breez_wc_check_pending_payments'); + } + + // Flush rewrite rules for webhook endpoint + flush_rewrite_rules(); +} + +/** + * Plugin deactivation hook + */ +function breez_wc_deactivate() { + // Unschedule payment status check + $timestamp = wp_next_scheduled('breez_wc_check_pending_payments'); + if ($timestamp) { + wp_unschedule_event($timestamp, 'breez_wc_check_pending_payments'); + } + + // Flush rewrite rules + flush_rewrite_rules(); +} + +// Check if WooCommerce is active before initializing +if (breez_wc_is_woocommerce_active()) { + // Initialize after WooCommerce is fully loaded + add_action('woocommerce_init', 'breez_wc_init'); + + // Register activation/deactivation hooks + register_activation_hook(__FILE__, 'breez_wc_activate'); + register_deactivation_hook(__FILE__, 'breez_wc_deactivate'); + + // Add gateway after WooCommerce payment gateways are registered + add_action('woocommerce_payment_gateways_init', function() { + add_filter('woocommerce_payment_gateways', 'breez_wc_add_gateway'); + }); + + // Register the payment gateway with WooCommerce Blocks + add_action('woocommerce_blocks_loaded', function() { + // Re-enabled with support for WooCommerce 9.8.3 + if (class_exists('Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType')) { + require_once BREEZ_WC_PLUGIN_DIR . 'includes/class-breez-blocks-support.php'; + + // Register scripts for blocks + add_action('wp_enqueue_scripts', 'breez_register_blocks_scripts'); + + add_action('woocommerce_blocks_payment_method_type_registration', function($payment_method_registry) { + $payment_method_registry->register(new Breez_Blocks_Support()); + + if (class_exists('Breez_Logger')) { + $logger = new Breez_Logger(true); + $logger->log('Registered Breez with WooCommerce Blocks', 'debug'); + } + }); + } + + // Log blocks integration + if (class_exists('Breez_Logger')) { + $logger = new Breez_Logger(true); + $logger->log('WooCommerce Blocks integration enabled for 9.8.3', 'debug'); + } + }); +} else { + // Display admin notice if WooCommerce is not active + add_action('admin_notices', 'breez_wc_woocommerce_missing_notice'); +} + +/** + * Register scripts for WooCommerce Blocks + */ +function breez_register_blocks_scripts() { + if (!is_checkout() && !is_cart()) { + return; + } + + $settings = get_option('woocommerce_breez_settings', []); + + // Only load if WooCommerce Blocks is active + if (!class_exists('Automattic\WooCommerce\Blocks\Package')) { + return; + } + + // Register the blocks script + wp_register_script( + 'breez-blocks', + BREEZ_WC_PLUGIN_URL . 'includes/block/breez-payment.js', + [ + 'wp-element', + 'wp-components', + 'wc-blocks-registry', + 'wp-blocks', + 'wp-i18n', + 'wp-html-entities', + 'wc-blocks-components', + 'wc-settings' + ], + BREEZ_WC_VERSION, + true + ); + + // Add settings for the blocks script + wp_localize_script( + 'breez-blocks', + 'wcSettings', + [ + 'breez' => [ + 'title' => !empty($settings['title']) ? $settings['title'] : 'Breez Nodeless Payments', + 'description' => !empty($settings['description']) ? $settings['description'] : 'Pay with Bitcoin via Lightning Network or on-chain transaction.', + 'supports' => ['products'], + 'showSavedCards' => false, + 'canMakePayment' => true, + 'paymentMethodId' => 'breez', + 'orderButtonText' => __('Proceed to Payment', 'breez-woocommerce'), + 'defaultPaymentMethod' => 'lightning', + 'paymentMethodData' => [ + 'breez_payment_method' => 'lightning' + ], + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('breez-payment') + ] + ] + ); + + // Enqueue the script + wp_enqueue_script('breez-blocks'); + + if (class_exists('Breez_Logger')) { + $logger = new Breez_Logger(true); + $logger->log('Breez blocks script registered and localized', 'debug'); + $logger->log('Script dependencies: ' . print_r(wp_scripts()->registered['breez-blocks']->deps, true), 'debug'); + } +} + +add_action('plugins_loaded', function() { + if (class_exists('Breez_Logger')) { + $logger = new Breez_Logger(true); + $logger->log('Breez plugin diagnostics running', 'debug'); + + // Check database tables + global $wpdb; + $table_name = $wpdb->prefix . 'wc_breez_payments'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name; + $logger->log("Database table check: " . ($table_exists ? 'exists' : 'missing'), 'debug'); + + // Check API settings + $api_url = get_option('woocommerce_breez_api_url'); + $api_key = get_option('woocommerce_breez_api_key'); + $payment_methods = get_option('woocommerce_breez_payment_methods'); + $logger->log("Configuration: API URL: " . (empty($api_url) ? 'missing' : 'set') . + ", API Key: " . (empty($api_key) ? 'missing' : 'set') . + ", Payment Methods: " . print_r($payment_methods, true), 'debug'); + } +}); + +// Add WooCommerce Blocks compatibility +add_action('before_woocommerce_init', function() { + if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('cart_checkout_blocks', __FILE__, true); + } +}); + +/** + * Handle AJAX request for payment URL + */ +function breez_handle_payment_url() { + check_ajax_referer('breez-payment', 'nonce'); + + $order_id = isset($_POST['order_id']) ? intval($_POST['order_id']) : 0; + $order = wc_get_order($order_id); + + if (!$order) { + wp_send_json_error(['message' => 'Order not found']); + return; + } + + $payment_url = $order->get_meta('_breez_payment_url'); + if (!$payment_url) { + wp_send_json_error(['message' => 'Payment URL not found']); + return; + } + + wp_send_json_success(['payment_url' => $payment_url]); +} +add_action('wp_ajax_breez_get_payment_url', 'breez_handle_payment_url'); +add_action('wp_ajax_nopriv_breez_get_payment_url', 'breez_handle_payment_url'); + +/** + * Set CORS headers for API endpoints + */ +function breez_wc_set_cors_headers() { + // Only set CORS headers for our endpoints + if (isset($_SERVER['REQUEST_URI']) && + (strpos($_SERVER['REQUEST_URI'], '/breez-wc/v1/') !== false || + strpos($_SERVER['REQUEST_URI'], '/wc-api/wc_gateway_breez') !== false)) { + + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); + header('Access-Control-Allow-Headers: Content-Type, X-API-Key, Authorization'); + + // Handle preflight requests + if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + status_header(200); + exit; + } + } +} +add_action('init', 'breez_wc_set_cors_headers', 1); + +/** + * Register blocks checkout data filter + */ +function breez_wc_register_blocks_checkout_filters() { + add_filter('woocommerce_store_api_checkout_update_order_from_request', 'breez_wc_filter_blocks_checkout_order_data', 10, 2); +} +add_action('init', 'breez_wc_register_blocks_checkout_filters'); + +/** + * Filter checkout API data to handle Breez payment data + * + * @param WC_Order $order + * @param WP_REST_Request $request + * @return WC_Order + */ +function breez_wc_filter_blocks_checkout_order_data($order, $request) { + if (class_exists('Breez_Logger')) { + $logger = new Breez_Logger(true); + $logger->log('Filtering checkout order data for blocks', 'debug'); + $logger->log('Request data: ' . print_r($request->get_params(), true), 'debug'); + } + + $params = $request->get_params(); + + // Check if Breez payment method is active + if (isset($params['payment_method']) && $params['payment_method'] === 'breez') { + if (isset($params['payment_data'])) { + // Get payment data + $payment_data = $params['payment_data']; + + // Store it as order meta for processing + $order->update_meta_data('_breez_blocks_payment_data', $payment_data); + $order->save(); + + if (class_exists('Breez_Logger')) { + $logger->log('Stored Breez payment data in order meta', 'debug'); + } + } + } + + return $order; +} diff --git a/class-wc-gateway-breez.php b/class-wc-gateway-breez.php new file mode 100644 index 0000000..cd4637d --- /dev/null +++ b/class-wc-gateway-breez.php @@ -0,0 +1,692 @@ +id = 'breez'; + $this->icon = apply_filters('woocommerce_breez_icon', ''); + $this->has_fields = false; + $this->method_title = __('Breez NodeLess Payments', 'breez-woocommerce'); + $this->method_description = __('Accept Lightning payments with Breez Nodeless SDK', 'breez-woocommerce'); + $this->supports = array( + 'products', + 'refunds' + ); + + // Initialize plugin settings + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables + $this->title = $this->get_option('title'); + $this->description = $this->get_option('description'); + $this->instructions = $this->get_option('instructions'); + $this->enabled = $this->get_option('enabled'); + $this->testmode = 'yes' === $this->get_option('testmode'); + $this->debug = 'yes' === $this->get_option('debug'); + $this->api_url = $this->get_option('api_url'); + $this->api_key = $this->get_option('api_key'); + $this->expiry_minutes = (int) $this->get_option('expiry_minutes', 30); + $this->payment_methods = $this->get_option('payment_methods', 'lightning,onchain'); + // Convert payment_methods to array if it's a string + if (is_string($this->payment_methods)) { + $this->payment_methods = array_filter(array_map('trim', explode(',', $this->payment_methods))); + } + + // Initialize logger first for comprehensive logging + $this->logger = new Breez_Logger($this->debug); + $this->logger->log('Initializing Breez Nodeless Payments gateway', 'debug'); + $this->logger->log('Current settings: ' . print_r($this->settings, true), 'debug'); + + // Validate API credentials + if ($this->enabled === 'yes' && (!$this->api_url || !$this->api_key)) { + $this->enabled = 'no'; + $this->logger->log('Gateway disabled - missing API credentials', 'error'); + $this->logger->log('API URL: ' . ($this->api_url ? $this->api_url : 'MISSING'), 'error'); + $this->logger->log('API Key: ' . ($this->api_key ? 'SET' : 'MISSING'), 'error'); + add_action('admin_notices', array($this, 'admin_api_notice')); + } + + // Initialize client, DB manager, payment handler + try { + $this->client = new Breez_API_Client( + $this->api_url, + $this->api_key + ); + + $this->db_manager = new Breez_DB_Manager(); + $this->payment_handler = new Breez_Payment_Handler($this->client, $this->db_manager); + + // Validate payment methods + if ($this->enabled === 'yes') { + if (empty($this->payment_methods)) { + $this->enabled = 'no'; + $this->logger->log('Gateway disabled - no payment methods selected', 'error'); + add_action('admin_notices', array($this, 'admin_payment_methods_notice')); + } else { + // Register webhook endpoint + add_action('woocommerce_api_wc_gateway_breez', array($this, 'webhook_handler')); + + // Initialize webhook handler + $this->webhook_handler = new Breez_Webhook_Handler($this->client, $this->db_manager); + } + } + } catch (Exception $e) { + $this->enabled = 'no'; + $this->logger->log('Gateway initialization failed: ' . $e->getMessage(), 'error'); + add_action('admin_notices', array($this, 'admin_api_error_notice')); + } + + // Hooks + add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options')); + add_action('woocommerce_thankyou_' . $this->id, array($this, 'thankyou_page')); + add_action('woocommerce_email_before_order_table', array($this, 'email_instructions'), 10, 3); + + // Register webhook URL with Breez API when settings are saved + add_action('woocommerce_settings_saved', array($this, 'register_webhook_url')); + + // Schedule payment status checks + if (!wp_next_scheduled('breez_check_pending_payments')) { + wp_schedule_event(time(), 'five_minutes', 'breez_check_pending_payments'); + } + + // WooCommerce Blocks integration is now registered in the main plugin file + // for better control over load order + } + + /** + * Initialize Gateway Settings Form Fields + */ + public function init_form_fields() { + $this->form_fields = require BREEZ_WC_PLUGIN_DIR . 'includes/admin/breez-settings.php'; + } + + /** + * Display admin notice for missing API credentials + */ + public function admin_api_notice() { + echo '

' . + __('Breez Nodeless Payments requires API URL and API Key to be configured. Please configure these in the gateway settings.', 'breez-woocommerce') . + '

'; + } + + /** + * Display admin notice for missing payment methods + */ + public function admin_payment_methods_notice() { + echo '

' . + __('Breez Nodeless Payments requires at least one payment method to be selected. Please configure payment methods in the gateway settings.', 'breez-woocommerce') . + '

'; + } + + /** + * Display admin notice for API initialization error + */ + public function admin_api_error_notice() { + echo '

' . + __('Breez Nodeless Payments encountered an error during initialization. Please check the logs for more details.', 'breez-woocommerce') . + '

'; + } + + /** + * Register webhook URL with Breez API + */ + public function register_webhook_url() { + if ('yes' !== $this->get_option('enabled')) { + return; + } + + try { + $webhook_url = get_rest_url(null, 'breez-wc/v1/webhook'); + $this->logger->log("Attempting to register webhook URL: $webhook_url", 'debug'); + + // Skip webhook registration in test mode + if ($this->testmode) { + $this->logger->log('Webhook registration skipped - test mode enabled', 'info'); + return; + } + + $result = $this->client->register_webhook($webhook_url); + if ($result) { + $this->logger->log("Successfully registered webhook URL", 'info'); + } else { + $this->logger->log("Failed to register webhook URL - webhooks may not be supported", 'warning'); + } + } catch (Exception $e) { + // Log error but don't block gateway activation + $this->logger->log("Webhook registration failed: " . $e->getMessage(), 'error'); + + // Only show admin notice if not a 404 (which likely means webhooks aren't supported) + if (strpos($e->getMessage(), '404') === false) { + add_action('admin_notices', function() { + echo '

' . + __('Breez webhook registration failed. Real-time payment notifications may not work.', 'breez-woocommerce') . + ' ' . __('The system will fall back to periodic payment status checks.', 'breez-woocommerce') . + '

'; + }); + } + } + } + + /** + * Handle webhook callback + */ + public function webhook_handler() { + if (!$this->webhook_handler) { + $this->logger->log('Webhook handler not initialized', 'error'); + wp_send_json_error('Webhook handler not initialized'); + return; + } + + $this->webhook_handler->process_webhook(); + } + + /** + * Get available payment methods as array + * + * @return array + */ + protected function get_available_payment_methods() { + if (empty($this->payment_methods)) { + return ['lightning']; // Default to lightning if nothing is set + } + + if (is_array($this->payment_methods)) { + return array_filter(array_map('trim', $this->payment_methods)); + } + + return array_filter(array_map('trim', explode(',', $this->payment_methods))); + } + + /** + * Process the payment and return the result + * + * @param int $order_id + * @return array + */ + public function process_payment($order_id) { + $this->logger->log("Processing payment for order #$order_id", 'debug'); + + try { + $order = wc_get_order($order_id); + if (!$order) { + throw new Exception(__('Order not found', 'breez-woocommerce')); + } + + // Get the selected payment method + $payment_method = $this->get_payment_method_from_request(); + + // Check for blocks payment data in order meta + $blocks_payment_data = $order->get_meta('_breez_blocks_payment_data'); + if (!$payment_method && $blocks_payment_data) { + $this->logger->log("Found blocks payment data in order meta", 'debug'); + $data = is_string($blocks_payment_data) ? json_decode($blocks_payment_data, true) : $blocks_payment_data; + if (is_array($data) && isset($data['breez_payment_method'])) { + $payment_method = sanitize_text_field($data['breez_payment_method']); + $this->logger->log("Using payment method from blocks data: $payment_method", 'debug'); + } + } + + if (!$payment_method) { + throw new Exception(__('No payment method selected', 'breez-woocommerce')); + } + $this->logger->log("Selected payment method: $payment_method", 'debug'); + + // Validate payment method + $available_methods = $this->get_available_payment_methods(); + if (!in_array($payment_method, $available_methods)) { + throw new Exception(__('Invalid payment method', 'breez-woocommerce')); + } + + // Get the order total and convert to satoshis + $order_total = $order->get_total(); + $currency = $order->get_currency(); + + // Get exchange rate and convert to satoshis + $exchange_rate_response = $this->client->request('GET', "/exchange_rates/{$currency}"); + if (!$exchange_rate_response || !isset($exchange_rate_response['rate'])) { + throw new Exception(__('Failed to get exchange rate', 'breez-woocommerce')); + } + + $btc_amount = $order_total / $exchange_rate_response['rate']; + $amount_sat = (int)($btc_amount * 100000000); // Convert BTC to sats + + $this->logger->log("Order total: {$order_total} {$currency}, Amount in sats: {$amount_sat}", 'debug'); + + if ($amount_sat <= 0) { + throw new Exception(__('Invalid amount conversion', 'breez-woocommerce')); + } + + // Create payment + $payment_data = array( + 'amount' => $amount_sat, // Send amount in satoshis + 'method' => $payment_method === 'onchain' ? 'BITCOIN_ADDRESS' : 'LIGHTNING', + 'description' => sprintf(__('Payment for order #%s', 'breez-woocommerce'), $order->get_order_number()) + ); + + // Log the payment data being sent + $this->logger->log("Creating payment with data: " . print_r($payment_data, true), 'debug'); + + // Create the payment + try { + $response = $this->client->request('POST', '/receive_payment', $payment_data); + $this->logger->log("Payment creation response: " . print_r($response, true), 'debug'); + + if (!$response || !isset($response['destination'])) { + throw new Exception(__('Failed to create payment', 'breez-woocommerce')); + } + + // Save payment data to order meta + $order->update_meta_data('_breez_payment_id', $response['destination']); + $order->update_meta_data('_breez_invoice_id', $response['destination']); + $order->update_meta_data('_breez_payment_method', $payment_method); + $order->update_meta_data('_breez_payment_amount', $order_total); + $order->update_meta_data('_breez_payment_amount_sat', $amount_sat); + $order->update_meta_data('_breez_payment_currency', $currency); + $order->update_meta_data('_breez_payment_status', 'pending'); + $order->update_meta_data('_breez_payment_created', time()); + $order->update_meta_data('_breez_payment_expires', time() + ($this->expiry_minutes * 60)); + + // Set order status to pending + $order->update_status('pending', __('Awaiting Bitcoin/Lightning payment', 'breez-woocommerce')); + + // Add order note + $order->add_order_note( + sprintf(__('Breez payment created. Method: %s, ID: %s, Amount: %s sats', 'breez-woocommerce'), + $payment_method, + $response['destination'], + $amount_sat + ) + ); + + // Save the order + $order->save(); + $this->logger->log("Order meta data updated for #$order_id", 'debug'); + + // Reduce stock levels + wc_reduce_stock_levels($order_id); + + // Empty cart + WC()->cart->empty_cart(); + + // Return success with redirect to order-received page + $return_url = $this->get_return_url($order); + $this->logger->log("Redirecting to: $return_url", 'debug'); + + return array( + 'result' => 'success', + 'redirect' => $return_url + ); + + } catch (Exception $e) { + $this->logger->log('Payment creation error: ' . $e->getMessage(), 'error'); + throw new Exception(__('Payment creation failed: ', 'breez-woocommerce') . $e->getMessage()); + } + } catch (Exception $e) { + $this->logger->log('Payment processing error: ' . $e->getMessage(), 'error'); + $order->update_status('failed', __('Payment error: ', 'breez-woocommerce') . $e->getMessage()); + return $this->get_error_response($e->getMessage()); + } + } + + /** + * Get payment method from request + * + * @return string + */ + public function get_payment_method_from_request() { + // Priority 1: Direct POST parameter for breez_payment_method (from blocks) + if (isset($_POST['breez_payment_method'])) { + $method = sanitize_text_field($_POST['breez_payment_method']); + $this->logger->log("Payment method found in direct POST: $method", 'debug'); + return $method; + } + + // Priority 2: Payment data from gateway form + if (isset($_POST['payment_method']) && $_POST['payment_method'] === 'breez') { + $this->logger->log('Processing checkout payment via standard form', 'debug'); + + // Check for payment data + if (isset($_POST['payment_data'])) { + $raw_data = wp_unslash($_POST['payment_data']); + $this->logger->log('Raw payment data: ' . print_r($raw_data, true), 'debug'); + + // Handle nested payment data structure + if (is_string($raw_data)) { + $decoded_data = json_decode($raw_data, true); + if (json_last_error() === JSON_ERROR_NONE) { + $raw_data = $decoded_data; + } + } + + // Extract payment method + if (is_array($raw_data) && isset($raw_data['breez_payment_method'])) { + $method = sanitize_text_field($raw_data['breez_payment_method']); + $this->logger->log("Payment method found in payment_data: $method", 'debug'); + return $method; + } + } + } + + // Priority 3: From WC Blocks checkout + if (isset($_POST['wc-breez-payment-data'])) { + $blocks_data = json_decode(wp_unslash($_POST['wc-breez-payment-data']), true); + if (is_array($blocks_data) && isset($blocks_data['breez_payment_method'])) { + $method = sanitize_text_field($blocks_data['breez_payment_method']); + $this->logger->log("Payment method found in blocks data: $method", 'debug'); + return $method; + } + } + + // Default fallback + $available_methods = $this->get_available_payment_methods(); + $default_method = reset($available_methods); // Get first available method + $this->logger->log("Using default payment method: $default_method", 'debug'); + return $default_method; + } + + /** + * Thank you page content + * + * @param int $order_id + */ + public function thankyou_page($order_id) { + $this->logger->log("Entering thankyou_page for order #$order_id", 'debug'); + + $order = wc_get_order($order_id); + if (!$order) { + $this->logger->log("Order #$order_id not found", 'error'); + return; + } + + // Only display for our payment method + if ($order->get_payment_method() !== $this->id) { + $this->logger->log("Order #$order_id payment method is not breez: " . $order->get_payment_method(), 'debug'); + return; + } + + // Get invoice ID + $invoice_id = $order->get_meta('_breez_invoice_id'); + if (!$invoice_id) { + $this->logger->log("No invoice ID found for order #$order_id", 'error'); + return; + } + + $payment_method = $order->get_meta('_breez_payment_method'); + $expiry = $order->get_meta('_breez_payment_expires'); + $amount_sat = $order->get_meta('_breez_payment_amount_sat'); + $current_time = time(); + + $this->logger->log("Payment details for order #$order_id:", 'debug'); + $this->logger->log("- Invoice ID: $invoice_id", 'debug'); + $this->logger->log("- Payment Method: $payment_method", 'debug'); + $this->logger->log("- Expiry: $expiry", 'debug'); + $this->logger->log("- Amount (sats): $amount_sat", 'debug'); + + // Check if payment is expired + if ($expiry && $current_time > $expiry) { + $this->logger->log("Payment expired for order #$order_id", 'debug'); + $order->update_status('failed', __('Lightning payment expired', 'breez-woocommerce')); + return; + } + + // Check current payment status + $payment_status = 'pending'; + try { + $payment_status = $this->client->check_payment_status($invoice_id); + $this->logger->log("Current payment status for order #$order_id: $payment_status", 'debug'); + } catch (Exception $e) { + $this->logger->log("Error checking payment status: " . $e->getMessage(), 'error'); + } + + // Update order status if needed + if ($payment_status === 'SUCCEEDED' && $order->get_status() === 'pending') { + $order->payment_complete(); + $order->add_order_note(__('Payment confirmed via Breez API.', 'breez-woocommerce')); + $this->logger->log("Payment for order #$order_id confirmed via API check", 'debug'); + } + + // Load the payment instructions template + $template_path = BREEZ_WC_PLUGIN_DIR . 'templates/payment-instructions.php'; + $this->logger->log("Loading template from: $template_path", 'debug'); + + if (!file_exists($template_path)) { + $this->logger->log("Template file not found!", 'error'); + return; + } + + // Load the payment instructions template + wc_get_template( + 'payment-instructions.php', + array( + 'order' => $order, + 'invoice_id' => $invoice_id, + 'payment_method' => $payment_method, + 'expiry' => $expiry, + 'current_time' => $current_time, + 'payment_status' => $payment_status + ), + '', + BREEZ_WC_PLUGIN_DIR . 'templates/' + ); + + $this->logger->log("Template loaded successfully for order #$order_id", 'debug'); + } + + /** + * Add content to the WC emails. + * + * @param WC_Order $order + * @param bool $sent_to_admin + * @param bool $plain_text + */ + public function email_instructions($order, $sent_to_admin, $plain_text = false) { + if ($sent_to_admin || $order->get_payment_method() !== $this->id || $order->has_status('processing')) { + return; + } + + $invoice_id = $order->get_meta('_breez_invoice_id'); + if (!$invoice_id) { + return; + } + + if ($plain_text) { + echo "\n\n" . $this->instructions . "\n\n"; + echo __('Invoice/Address: ', 'breez-woocommerce') . $invoice_id . "\n"; + } else { + echo '

' . __('Payment Information', 'breez-woocommerce') . '

'; + echo '

' . $this->instructions . '

'; + echo '

' . __('Invoice/Address: ', 'breez-woocommerce') . ' ' . $invoice_id . '

'; + } + } + + /** + * Check if this gateway is available + * + * @return bool + */ + public function is_available() { + // Get fresh settings from database + $this->init_settings(); + $api_url = $this->get_option('api_url'); + $api_key = $this->get_option('api_key'); + $payment_methods = $this->get_option('payment_methods'); + + $this->logger->log("Gateway availability check", 'debug'); + $this->logger->log("Settings from DB: api_url=" . ($api_url ? $api_url : 'MISSING') . + ", api_key=" . ($api_key ? 'SET' : 'MISSING') . + ", payment_methods=" . print_r($payment_methods, true), 'debug'); + + if ($this->enabled === 'no') { + $this->logger->log("Gateway disabled in settings", 'debug'); + return false; + } + + // Check API credentials + if (empty($api_url) || empty($api_key)) { + $this->logger->log("API credentials missing - URL: " . (empty($api_url) ? 'No' : 'Yes') . + ", Key: " . (empty($api_key) ? 'No' : 'Yes'), 'debug'); + return false; + } + + // Update class variables with fresh settings + $this->api_url = $api_url; + $this->api_key = $api_key; + $this->payment_methods = $payment_methods; + + // Check payment methods + if (empty($this->payment_methods)) { + $this->logger->log("No payment methods selected", 'debug'); + return false; + } + + // Check payment handler + if (!isset($this->payment_handler) || !$this->payment_handler) { + $this->logger->log("Payment handler not initialized", 'debug'); + + // Try to initialize payment handler on-demand + try { + if (!isset($this->client)) { + $this->client = new Breez_API_Client($this->api_url, $this->api_key); + } + if (!isset($this->db_manager)) { + $this->db_manager = new Breez_DB_Manager(); + } + $this->payment_handler = new Breez_Payment_Handler($this->client, $this->db_manager); + } catch (Exception $e) { + $this->logger->log("Failed to initialize payment handler: " . $e->getMessage(), 'error'); + return false; + } + } + + // Check exchange rate + try { + $rate = $this->payment_handler->get_btc_rate(); + if (!$rate) { + $this->logger->log("Unable to get exchange rate", 'error'); + return false; + } + $this->logger->log("Exchange rate: $rate", 'debug'); + } catch (Exception $e) { + $this->logger->log("Exchange rate error: " . $e->getMessage(), 'error'); + return false; + } + + $this->logger->log("Gateway available", 'debug'); + return true; + } + + + /** + * Process a refund if supported + * + * @param int $order_id + * @param float $amount + * @param string $reason + * @return bool + */ + public function process_refund($order_id, $amount = null, $reason = '') { + $order = wc_get_order($order_id); + + if (!$order) { + $this->logger->log("Error: Order #$order_id not found for refund"); + return false; + } + + $this->logger->log("Manual refund requested for order #$order_id, amount: $amount, reason: $reason"); + + // For now, just log the request and notify that manual refund is required + $order->add_order_note( + sprintf( + __('Refund of %s requested. Bitcoin/Lightning payments require manual refund processing. Reason: %s', 'breez-woocommerce'), + wc_price($amount), + $reason + ) + ); + + return true; + } + + /** + * Reduce stock levels + * + * @param int $order_id + */ + public function reduce_stock_levels($order_id) { + $order = wc_get_order($order_id); + + if ($order) { + $this->logger->log("Reducing stock levels for order #{$order_id}", 'debug'); + wc_reduce_stock_levels($order_id); + $this->logger->log("Stock levels reduced for order #{$order_id}", 'info'); + } else { + $this->logger->log("Failed to reduce stock: Order #{$order_id} not found", 'error'); + } + } + + /** + * Get a standardized error response + * + * @param string $message Error message + * @return array + */ + protected function get_error_response($message = '') { + $this->logger->log('Payment error: ' . $message, 'error'); + + return array( + 'result' => 'failure', + 'redirect' => '', + 'message' => $message ? $message : __('An error occurred during the payment process.', 'breez-woocommerce'), + ); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2c6dd5e --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "breez/woocommerce", + "description": "Bitcoin & Lightning payment gateway for WooCommerce using Breez Nodeless API", + "type": "wordpress-plugin", + "require": { + "php": ">=7.2", + "aferrandini/phpqrcode": "^1.0" + }, + "autoload": { + "psr-4": { + "Breez\\WooCommerce\\": "includes/" + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + } +} \ No newline at end of file diff --git a/includes/admin/breez-settings.php b/includes/admin/breez-settings.php new file mode 100644 index 0000000..2224760 --- /dev/null +++ b/includes/admin/breez-settings.php @@ -0,0 +1,107 @@ + array( + 'title' => __('Enable/Disable', 'breez-woocommerce'), + 'type' => 'checkbox', + 'label' => __('Enable Breez Nodeless Payments', 'breez-woocommerce'), + 'default' => 'no' + ), + 'title' => array( + 'title' => __('Title', 'breez-woocommerce'), + 'type' => 'text', + 'description' => __('This controls the title which the user sees during checkout.', 'breez-woocommerce'), + 'default' => __('Breez Nodeless Payments', 'breez-woocommerce'), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __('Description', 'breez-woocommerce'), + 'type' => 'textarea', + 'description' => __('This controls the description which the user sees during checkout.', 'breez-woocommerce'), + 'default' => __('Pay with Bitcoin via Lightning Network or on-chain transaction.', 'breez-woocommerce'), + 'desc_tip' => true, + ), + 'instructions' => array( + 'title' => __('Instructions', 'breez-woocommerce'), + 'type' => 'textarea', + 'description' => __('Instructions that will be added to the thank you page and emails.', 'breez-woocommerce'), + 'default' => __('Scan the QR code or copy the invoice/address to complete your payment.', 'breez-woocommerce'), + 'desc_tip' => true, + ), + 'api_settings' => array( + 'title' => __('API Settings', 'breez-woocommerce'), + 'type' => 'title', + 'description' => __('Enter your Breez API credentials below.', 'breez-woocommerce'), + ), + 'api_url' => array( + 'title' => __('API URL', 'breez-woocommerce'), + 'type' => 'text', + 'description' => __('Enter your Breez API URL.', 'breez-woocommerce'), + 'default' => 'http://localhost:8000', + 'desc_tip' => true, + ), + 'api_key' => array( + 'title' => __('API Key', 'breez-woocommerce'), + 'type' => 'password', + 'description' => __('Enter your Breez API key.', 'breez-woocommerce'), + 'default' => '', + 'desc_tip' => true, + ), + 'payment_options' => array( + 'title' => __('Payment Options', 'breez-woocommerce'), + 'type' => 'title', + 'description' => __('Configure payment behavior.', 'breez-woocommerce'), + ), + 'payment_methods' => array( + 'title' => __('Payment Methods', 'breez-woocommerce'), + 'type' => 'multiselect', + 'class' => 'wc-enhanced-select', + 'description' => __('Select the payment methods you want to accept.', 'breez-woocommerce'), + 'default' => array('lightning', 'onchain'), + 'options' => array( + 'lightning' => __('Lightning Network', 'breez-woocommerce'), + 'onchain' => __('On-chain Bitcoin', 'breez-woocommerce'), + ), + 'desc_tip' => true, + ), + 'expiry_minutes' => array( + 'title' => __('Invoice Expiry Time', 'breez-woocommerce'), + 'type' => 'number', + 'description' => __('Number of minutes after which unpaid invoices expire.', 'breez-woocommerce'), + 'default' => 30, + 'desc_tip' => true, + 'custom_attributes' => array( + 'min' => 5, + 'max' => 1440, + 'step' => 1, + ), + ), + 'advanced_options' => array( + 'title' => __('Advanced Options', 'breez-woocommerce'), + 'type' => 'title', + 'description' => __('Additional settings for advanced users.', 'breez-woocommerce'), + ), + 'debug' => array( + 'title' => __('Debug Log', 'breez-woocommerce'), + 'type' => 'checkbox', + 'label' => __('Enable logging', 'breez-woocommerce'), + 'default' => 'no', + 'description' => __('Log Breez payment events, such as API requests, inside the WooCommerce logs directory.', 'breez-woocommerce'), + ), + 'testmode' => array( + 'title' => __('Test Mode', 'breez-woocommerce'), + 'type' => 'checkbox', + 'label' => __('Enable Test Mode', 'breez-woocommerce'), + 'default' => 'no', + 'description' => __('Place the payment gateway in test mode.', 'breez-woocommerce'), + ), +); \ No newline at end of file diff --git a/includes/block/breez-payment.asset.php b/includes/block/breez-payment.asset.php new file mode 100644 index 0000000..b00e0cb --- /dev/null +++ b/includes/block/breez-payment.asset.php @@ -0,0 +1,10 @@ + array( + 'wp-blocks', + 'wp-element', + 'wp-components', + 'wc-blocks-registry' + ), + 'version' => BREEZ_WC_VERSION +); \ No newline at end of file diff --git a/includes/block/breez-payment.js b/includes/block/breez-payment.js new file mode 100644 index 0000000..a454f38 --- /dev/null +++ b/includes/block/breez-payment.js @@ -0,0 +1,86 @@ +/** + * Breez Payment Method for WooCommerce Blocks + */ +const { registerPaymentMethod } = window.wc.wcBlocksRegistry; +const { createElement, useEffect } = window.wp.element; +const { Fragment } = window.wp.element; +const { decodeEntities } = window.wp.htmlEntities; +const { PaymentMethodLabel, PaymentMethodIcon } = window.wc.blocksComponents || {}; + +const BreezLabel = () => { + return createElement('div', { className: 'wc-block-components-payment-method-label' }, + createElement('span', {}, window.wcSettings?.breez?.title || 'Breez Nodeless Payments') + ); +}; + +const BreezComponent = () => { + const description = window.wcSettings?.breez?.description || 'Pay with Bitcoin via Lightning Network or on-chain transaction.'; + return createElement('div', { className: 'wc-block-components-payment-method-description' }, + decodeEntities(description) + ); +}; + +// Payment processing component +const ProcessPayment = ({ eventRegistration }) => { + const { onPaymentProcessing } = eventRegistration; + + useEffect(() => { + const unsubscribe = onPaymentProcessing(() => { + console.log('Breez payment processing started'); + try { + return { + type: 'success', + meta: { + paymentMethodData: { + payment_method: 'breez', + breez_payment_method: window.wcSettings?.breez?.defaultPaymentMethod || 'lightning' + } + } + }; + } catch (error) { + console.error('Breez payment processing error:', error); + return { + type: 'error', + message: error.message || 'Payment processing failed' + }; + } + }); + + return () => { + unsubscribe(); + }; + }, [onPaymentProcessing]); + + return null; +}; + +// Register the Breez payment method +const breezPaymentMethod = { + name: 'breez', + label: createElement(BreezLabel), + content: createElement(BreezComponent), + edit: createElement(BreezComponent), + canMakePayment: () => true, + ariaLabel: window.wcSettings?.breez?.title || 'Breez Nodeless Payments', + paymentMethodId: 'breez', + supports: { + features: window.wcSettings?.breez?.supports || ['products'], + showSavedCards: false, + showSaveOption: false + }, + billing: { + required: false + }, + data: { + breez_payment_method: window.wcSettings?.breez?.defaultPaymentMethod || 'lightning' + }, + placeOrderButtonLabel: window.wcSettings?.breez?.orderButtonText || 'Pay with Bitcoin', + processPayment: createElement(ProcessPayment) +}; + +try { + registerPaymentMethod(breezPaymentMethod); + console.log('Breez payment method registered successfully'); +} catch (error) { + console.error('Failed to register Breez payment method:', error); +} \ No newline at end of file diff --git a/includes/block/breez-widget.asset.php b/includes/block/breez-widget.asset.php new file mode 100644 index 0000000..b3a9b5d --- /dev/null +++ b/includes/block/breez-widget.asset.php @@ -0,0 +1,8 @@ + array( + 'wp-polyfill', + 'jquery' + ), + 'version' => BREEZ_WC_VERSION +); \ No newline at end of file diff --git a/includes/block/breez-widget.js b/includes/block/breez-widget.js new file mode 100644 index 0000000..5d98fb5 --- /dev/null +++ b/includes/block/breez-widget.js @@ -0,0 +1,8 @@ +/** + * Breez Payment Widget for WooCommerce Blocks + */ +(function ($) { + $(document).ready(function() { + // Initialize any client-side functionality here + }); +})(jQuery); \ No newline at end of file diff --git a/includes/class-breez-api-client.php b/includes/class-breez-api-client.php new file mode 100644 index 0000000..effe281 --- /dev/null +++ b/includes/class-breez-api-client.php @@ -0,0 +1,464 @@ +api_url = rtrim($api_url, '/'); + $this->api_key = $api_key; + $this->logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); + + if (!$this->api_url || !$this->api_key) { + $this->logger->log('API client initialized without credentials', 'error'); + throw new Exception('API credentials are required'); + } + + // Validate API URL format + if (!filter_var($this->api_url, FILTER_VALIDATE_URL)) { + $this->logger->log('Invalid API URL format: ' . $this->api_url, 'error'); + throw new Exception('API URL must be a valid URL'); + } + + $this->logger->log('API client initialized', 'debug', array( + 'api_url' => $this->api_url, + 'api_key_set' => !empty($this->api_key) + )); + } + + /** + * Check API connectivity + * + * @return bool True if API is accessible, false otherwise + */ + public function check_health() { + // In test mode, bypass the health check + if ('yes' === get_option('woocommerce_breez_testmode', 'no')) { + $this->logger->log('API health check bypassed in test mode', 'debug'); + return true; + } + try { + $this->logger->log('Starting API health check', 'debug'); + $response = $this->request('GET', '/health'); + $this->logger->log('API health check response: ' . json_encode($response), 'debug'); + return isset($response['status']) && $response['status'] === 'ok'; + } catch (Exception $e) { + $this->logger->log('API health check failed: ' . $e->getMessage(), 'error'); + $this->logger->log('Stack trace: ' . $e->getTraceAsString(), 'debug'); + return false; + } + } + + /** + * Generate a payment request + * + * @param int $amount Amount in satoshis + * @param string $method Payment method (LIGHTNING, BITCOIN_ADDRESS, LIQUID_ADDRESS) + * @param string $description Payment description + * @return array|false Payment details on success, false on failure + */ + public function receive_payment($amount, $method = 'LIGHTNING', $description = '') { + $data = array( + 'amount' => $amount, + 'method' => $method, + 'description' => $description + ); + + return $this->request('POST', '/receive_payment', $data); + } + + /** + * Check payment status using API endpoint + * + * @param string $invoice_id Invoice ID or payment destination + * @return array Payment status response + */ + public function check_payment_status($invoice_id) { + try { + $response = $this->request('GET', "/check_payment_status/{$invoice_id}"); + + if (!$response) { + throw new Exception('Failed to check payment status'); + } + + // Log the raw API response for debugging + $this->logger->log('Payment status check response', 'debug', array( + 'invoice_id' => $invoice_id, + 'response' => $response + )); + + // If the payment is not found, return pending instead of throwing an error + if (isset($response['error']) && $response['error'] === 'Payment not found') { + return array( + 'status' => 'pending', + 'destination' => $invoice_id + ); + } + + // Return the response as is + return $response; + + } catch (Exception $e) { + $this->logger->log('Payment status check error: ' . $e->getMessage(), 'error'); + // Return pending status instead of throwing an error + return array( + 'status' => 'pending', + 'destination' => $invoice_id + ); + } + } + + /** + * Register a webhook URL + * + * @param string $webhook_url Webhook URL + * @return array|false Response on success, false on failure + */ + /** + * Check if webhooks are supported by the API + * + * @return bool True if webhooks are supported + */ + public function check_webhook_support() { + try { + // Try to get API endpoints/capabilities + $response = $this->request('GET', '/capabilities', array(), 1); + + // If capabilities endpoint exists, check webhook support + if ($response && isset($response['features'])) { + return in_array('webhooks', $response['features']); + } + + // If capabilities endpoint doesn't exist, try webhook endpoint directly + $this->request('GET', '/register_webhook', array(), 1); + return true; + + } catch (Exception $e) { + // If we get a 404, webhooks aren't supported + if (strpos($e->getMessage(), '404') !== false) { + $this->logger->log('Webhooks not supported by API', 'debug'); + return false; + } + + // For other errors, assume webhooks might be supported + $this->logger->log('Webhook support check failed: ' . $e->getMessage(), 'warning'); + return true; + } + } + + /** + * Register a webhook URL + * + * @param string $webhook_url Webhook URL + * @return bool True if registration successful + * @throws Exception if registration fails + */ + public function register_webhook($webhook_url) { + if (!$webhook_url) { + throw new Exception('Webhook URL is required'); + } + + $this->logger->log('Registering webhook', 'debug', array( + 'url' => $webhook_url + )); + + try { + $data = array( + 'webhook_url' => $webhook_url + ); + + $response = $this->request('POST', '/register_webhook', $data, 1); + + if ($response && isset($response['success']) && $response['success']) { + $this->logger->log('Webhook registration successful', 'info'); + return true; + } + + $this->logger->log('Webhook registration failed - invalid response', 'error'); + return false; + + } catch (Exception $e) { + // If we get a 404, webhooks aren't supported + if (strpos($e->getMessage(), '404') !== false) { + $this->logger->log('Webhook registration failed - endpoint not found', 'debug'); + return false; + } + + // Re-throw other errors + throw $e; + } + } + + /** + * Get payment by ID + * + * @param string $payment_hash Payment hash + * @return array|false Payment details on success, false on failure + */ + public function get_payment($payment_hash) { + return $this->request('GET', "/payment/{$payment_hash}"); + } + + /** + * List all payments + * + * @param array $params Optional query parameters + * @return array|false List of payments on success, false on failure + */ + public function list_payments($params = array()) { + $query_string = ''; + if (!empty($params)) { + $query_string = '?' . http_build_query($params); + } + + return $this->request('GET', "/list_payments{$query_string}"); + } + + /** + * Make API request + * + * @param string $method HTTP method + * @param string $endpoint API endpoint + * @param array $data Request data + * @param int $max_retries Maximum number of retries + * @return array|false Response data on success, false on failure + */ + public function request($method, $endpoint, $data = array(), $max_retries = 2) { + // Normalize the endpoint to ensure it starts with a slash + $endpoint = ltrim($endpoint, '/'); + + // Full API URL + $url = $this->api_url . '/' . $endpoint; + + $this->logger->log('Making API request', 'debug', array( + 'method' => $method, + 'url' => $url + )); + + $args = array( + 'method' => $method, + 'timeout' => 30, + 'headers' => array( + 'Content-Type' => 'application/json', + 'x-api-key' => $this->api_key, + 'Accept' => 'application/json' + ) + ); + + if (!empty($data) && $method !== 'GET') { + $args['body'] = json_encode($data); + } elseif (!empty($data) && $method === 'GET') { + // For GET requests, append query string + $url = add_query_arg($data, $url); + } + + $retries = 0; + $response_data = false; + + while ($retries <= $max_retries) { + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + $error_message = $response->get_error_message(); + $this->logger->log('API request error', 'error', array( + 'message' => $error_message, + 'attempt' => $retries + 1 + )); + + $retries++; + if ($retries <= $max_retries) { + $this->logger->log('Retrying request', 'debug', array( + 'attempt' => $retries + )); + // Exponential backoff + sleep(pow(2, $retries - 1)); + continue; + } + + throw new Exception('API request failed: ' . $error_message); + } + + $http_code = wp_remote_retrieve_response_code($response); + $body = wp_remote_retrieve_body($response); + + $this->logger->log('API response received', 'debug', array( + 'http_code' => $http_code, + 'body_length' => strlen($body) + )); + + if ($http_code >= 200 && $http_code < 300) { + if (!empty($body)) { + $response_data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->logger->log('JSON decode error', 'error', array( + 'error' => json_last_error_msg(), + 'body_excerpt' => substr($body, 0, 100) . (strlen($body) > 100 ? '...' : '') + )); + + // If we can't decode JSON, try to return the raw body + $response_data = array( + 'raw_response' => $body + ); + } + } else { + // Empty but successful response + $response_data = array( + 'success' => true + ); + } + + // Success - break out of retry loop + break; + } else { + // Handle error + $message = $body; + + if (!empty($body)) { + $decoded = json_decode($body, true); + if (is_array($decoded) && isset($decoded['message'])) { + $message = $decoded['message']; + } elseif (is_array($decoded) && isset($decoded['error'])) { + $message = $decoded['error']; + } + } + + // 404 might be normal in some cases (checking if endpoint exists) + $error_level = ($http_code == 404) ? 'debug' : 'error'; + $this->logger->log('API error response', $error_level, array( + 'http_code' => $http_code, + 'message' => $message, + 'endpoint' => $endpoint, + 'attempt' => $retries + 1 + )); + + if ($http_code == 404 || ($http_code >= 500 && $retries < $max_retries)) { + $retries++; + if ($retries <= $max_retries) { + $this->logger->log('Retrying request', 'debug', array( + 'attempt' => $retries + )); + // Exponential backoff + sleep(pow(2, $retries - 1)); + continue; + } + } + + throw new Exception("API error ($http_code): $message"); + } + } + + return $response_data; + } + + /** + * Create a new payment + * + * @param array $payment_data Payment data including amount, currency, description, etc. + * @return array Payment details + * @throws Exception if payment creation fails + */ + public function create_payment($payment_data) { + $this->logger->log('Creating payment with data: ' . print_r($payment_data, true), 'debug'); + + try { + // Prepare the API request data according to ReceivePaymentBody schema + $api_data = array( + 'amount' => $payment_data['amount_sat'], // Amount must be in satoshis + 'method' => strtoupper($payment_data['payment_method']), // LIGHTNING or BITCOIN_ADDRESS + 'description' => $payment_data['description'] ?? '', + ); + + // Make the API request to create payment + $response = $this->request('POST', '/receive_payment', $api_data); + + if (!$response || !isset($response['destination'])) { + throw new Exception('Invalid API response: Missing payment destination'); + } + + // Format the response to match what the gateway expects + return array( + 'id' => $response['destination'], // Use destination as ID + 'invoice_id' => $response['destination'], + 'payment_url' => $response['destination'], // For QR code generation + 'payment_request' => $response['destination'], + 'status' => 'PENDING', + 'amount' => $payment_data['amount'], + 'amount_sat' => $payment_data['amount_sat'], + 'currency' => $payment_data['currency'], + 'created_at' => time(), + 'expires_at' => time() + ($payment_data['expires_in'] ?? 1800), + 'fees_sat' => $response['fees_sat'] ?? 0 + ); + + } catch (Exception $e) { + $this->logger->log('Payment creation failed: ' . $e->getMessage(), 'error'); + throw new Exception('Failed to create payment: ' . $e->getMessage()); + } + } + + /** + * Convert fiat amount to satoshis + * + * @param float $amount Amount in fiat + * @param string $currency Currency code + * @return int Amount in satoshis + * @throws Exception if conversion fails + */ + private function convert_to_sats($amount, $currency) { + try { + // Try to get the exchange rate from the API + $response = $this->request('GET', '/exchange_rates/' . strtoupper($currency)); + + if (!$response || !isset($response['rate'])) { + throw new Exception('Invalid exchange rate response'); + } + + // Calculate satoshis + $btc_amount = $amount / $response['rate']; + return (int)($btc_amount * 100000000); // Convert BTC to sats + + } catch (Exception $e) { + $this->logger->log('Currency conversion failed: ' . $e->getMessage(), 'error'); + throw new Exception('Failed to convert currency: ' . $e->getMessage()); + } + } +} diff --git a/includes/class-breez-blocks-support.php b/includes/class-breez-blocks-support.php new file mode 100644 index 0000000..9703ff6 --- /dev/null +++ b/includes/class-breez-blocks-support.php @@ -0,0 +1,224 @@ +settings = get_option('woocommerce_breez_settings', []); + + // Initialize logger + $this->logger = new Breez_Logger(true); + $this->logger->log('Initializing Breez Blocks Support', 'debug'); + + // Create payment gateway instance if not exists + if (!isset($this->gateway) && class_exists('WC_Gateway_Breez')) { + $this->gateway = new WC_Gateway_Breez(); + } + + // Log initialization for debugging + if (isset($this->gateway)) { + $this->logger->log('Gateway instance created successfully', 'debug'); + $this->logger->log('Current settings: ' . print_r($this->settings, true), 'debug'); + } else { + $this->logger->log('Failed to create gateway instance', 'error'); + } + } + + /** + * Check if this payment method is active + * + * @return bool + */ + public function is_active() { + $is_active = false; + + if (isset($this->gateway)) { + $is_active = $this->gateway->is_available(); + $this->logger->log('Gateway availability check: ' . ($is_active ? 'yes' : 'no'), 'debug'); + + if (!$is_active) { + $this->logger->log('Gateway not available. Settings: ' . print_r($this->settings, true), 'debug'); + } + } else { + $is_active = !empty($this->settings['enabled']) && 'yes' === $this->settings['enabled']; + $this->logger->log('Fallback availability check: ' . ($is_active ? 'yes' : 'no'), 'debug'); + } + + return $is_active; + } + + /** + * Returns an array of scripts/handles to be registered for this payment method. + * + * @return array + */ + public function get_payment_method_script_handles() { + $this->logger->log('Registering payment method script handles', 'debug'); + + $handles = []; + + // Register and enqueue the widget script + $widget_handle = 'breez-widget'; + $widget_asset_file = plugin_dir_path(__FILE__) . 'block/breez-widget.asset.php'; + $widget_asset_data = file_exists($widget_asset_file) ? require($widget_asset_file) : array('dependencies' => array('wp-polyfill', 'jquery'), 'version' => '1.0.0'); + + wp_register_script( + $widget_handle, + plugins_url('block/breez-widget.js', __FILE__), + $widget_asset_data['dependencies'], + $widget_asset_data['version'], + true + ); + + // Register and enqueue the payment script + $payment_handle = 'breez-blocks'; + $payment_asset_file = plugin_dir_path(__FILE__) . 'block/breez-payment.asset.php'; + $payment_asset_data = file_exists($payment_asset_file) ? require($payment_asset_file) : array('dependencies' => array('wp-blocks', 'wp-element', 'wp-components', 'wc-blocks-registry'), 'version' => '1.0.0'); + + wp_register_script( + $payment_handle, + plugins_url('block/breez-payment.js', __FILE__), + $payment_asset_data['dependencies'], + $payment_asset_data['version'], + true + ); + + if (function_exists('wp_set_script_translations')) { + wp_set_script_translations($widget_handle, 'breez-woocommerce'); + wp_set_script_translations($payment_handle, 'breez-woocommerce'); + } + + $handles[] = $widget_handle; + $handles[] = $payment_handle; + + return $handles; + } + + /** + * Returns an array of key=>value pairs of data made available to the payment methods + * script. + * + * @return array + */ + public function get_payment_method_data() { + $data = [ + 'title' => !empty($this->settings['title']) ? $this->settings['title'] : 'Breez Nodeless Payments', + 'description' => !empty($this->settings['description']) ? $this->settings['description'] : 'Pay with Bitcoin via Lightning Network or on-chain transaction.', + 'supports' => ['products'], + 'showSavedCards' => false, + 'canMakePayment' => true, + 'paymentMethodId' => 'breez', + 'orderButtonText' => __('Proceed to Payment', 'breez-woocommerce'), + 'defaultPaymentMethod' => 'lightning' + ]; + + $this->logger->log('Payment method data: ' . print_r($data, true), 'debug'); + return $data; + } + + /** + * Process the payment + * + * @param \WC_Order $order + * @param array $payment_data + * @return array + */ + public function process_payment($order, $payment_data) { + try { + $this->logger->log('Processing payment in Blocks Support', 'debug'); + $this->logger->log('Order ID: ' . $order->get_id(), 'debug'); + $this->logger->log('Payment Data: ' . print_r($payment_data, true), 'debug'); + + if (!isset($this->gateway)) { + $this->gateway = new WC_Gateway_Breez(); + } + + if (!$this->gateway) { + throw new Exception(__('Payment gateway not initialized', 'breez-woocommerce')); + } + + // Get payment method from blocks data + $payment_method = 'lightning'; // Default to lightning + if (isset($payment_data['paymentMethodData']['data']['breez_payment_method'])) { + $payment_method = sanitize_text_field($payment_data['paymentMethodData']['data']['breez_payment_method']); + } + + // Store payment method in POST for gateway processing + $_POST['breez_payment_method'] = $payment_method; + + // Store any additional payment data + $order->update_meta_data('_breez_blocks_payment_data', wp_json_encode([ + 'payment_method' => $payment_method, + 'timestamp' => time() + ])); + $order->save(); + + $this->logger->log('Selected payment method: ' . $payment_method, 'debug'); + + // Process the payment + $result = $this->gateway->process_payment($order->get_id()); + + if (!is_array($result)) { + throw new Exception(__('Invalid payment gateway response', 'breez-woocommerce')); + } + + $this->logger->log('Payment processing result: ' . print_r($result, true), 'debug'); + + // Ensure we have a proper response format + if (!isset($result['result'])) { + $result['result'] = 'success'; + } + + return $result; + + } catch (Exception $e) { + $this->logger->log('Payment processing error: ' . $e->getMessage(), 'error'); + $this->logger->log('Error trace: ' . $e->getTraceAsString(), 'debug'); + + // Return a properly formatted error response + return [ + 'result' => 'failure', + 'messages' => [ + 'error' => [ + 'message' => $e->getMessage() + ] + ], + 'redirect' => wc_get_cart_url() + ]; + } + } +} \ No newline at end of file diff --git a/includes/class-breez-db-manager.php b/includes/class-breez-db-manager.php new file mode 100644 index 0000000..0f9cdc5 --- /dev/null +++ b/includes/class-breez-db-manager.php @@ -0,0 +1,261 @@ +table_name = $wpdb->prefix . 'wc_breez_payments'; + $this->logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); + } + +/** + * Install database tables + */ +public function install_tables() { + global $wpdb; + + $charset_collate = $wpdb->get_charset_collate(); + + $this->logger->log("Installing database tables with charset: {$charset_collate}", 'debug'); + + $table_name = $wpdb->prefix . 'wc_breez_payments'; + $this->logger->log("Table name: {$table_name}", 'debug'); + + $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + order_id BIGINT UNSIGNED NOT NULL, + invoice_id VARCHAR(255) NOT NULL, + amount DECIMAL(16,8) NOT NULL, + currency VARCHAR(10) NOT NULL, + status VARCHAR(20) NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + metadata TEXT, + PRIMARY KEY (id), + UNIQUE KEY order_id (order_id), + KEY invoice_id (invoice_id) + ) $charset_collate;"; + + $this->logger->log("SQL query: {$sql}", 'debug'); + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + $result = dbDelta($sql); + + $this->logger->log("Database tables installation result: " . json_encode($result), 'debug'); + + // Check if table was created successfully + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name; + $this->logger->log("Table exists after creation: " . ($table_exists ? 'yes' : 'no'), 'debug'); + + if (!$table_exists) { + $this->logger->log("Failed to create database table", 'error'); + $this->logger->log("WordPress DB error: " . $wpdb->last_error, 'error'); + } else { + $this->logger->log("Database tables installed successfully", 'info'); + } +} + + /** + * Save payment data + * + * @param int $order_id Order ID + * @param string $invoice_id Invoice ID + * @param float $amount Payment amount + * @param string $currency Currency code + * @param string $status Payment status + * @param array $metadata Optional metadata + * @return bool Success/failure + */ + public function save_payment($order_id, $invoice_id, $amount, $currency, $status, $metadata = array()) { + global $wpdb; + + $now = current_time('mysql'); + + try { + $result = $wpdb->insert( + $this->table_name, + array( + 'order_id' => $order_id, + 'invoice_id' => $invoice_id, + 'amount' => $amount, + 'currency' => $currency, + 'status' => $status, + 'created_at' => $now, + 'updated_at' => $now, + 'metadata' => $metadata ? json_encode($metadata) : null + ), + array('%d', '%s', '%f', '%s', '%s', '%s', '%s', '%s') + ); + + if ($result) { + $this->logger->log("Payment saved: order_id=$order_id, invoice_id=$invoice_id, status=$status"); + return true; + } else { + $this->logger->log("Error saving payment: " . $wpdb->last_error); + return false; + } + } catch (Exception $e) { + $this->logger->log("Exception saving payment: " . $e->getMessage()); + return false; + } + } + + /** + * Update payment status + * + * @param int $order_id Order ID + * @param string $status New status + * @return bool Success/failure + */ + public function update_payment_status($order_id, $status) { + global $wpdb; + + $now = current_time('mysql'); + + try { + $result = $wpdb->update( + $this->table_name, + array( + 'status' => $status, + 'updated_at' => $now + ), + array('order_id' => $order_id), + array('%s', '%s'), + array('%d') + ); + + if ($result !== false) { + $this->logger->log("Payment status updated: order_id=$order_id, new_status=$status"); + return true; + } else { + $this->logger->log("Error updating payment status: " . $wpdb->last_error); + return false; + } + } catch (Exception $e) { + $this->logger->log("Exception updating payment status: " . $e->getMessage()); + return false; + } + } + + /** + * Get payment by invoice ID + * + * @param string $invoice_id Invoice ID + * @return array|false Payment data or false if not found + */ + public function get_payment_by_invoice($invoice_id) { + global $wpdb; + + try { + $query = $wpdb->prepare( + "SELECT * FROM {$this->table_name} WHERE invoice_id = %s", + $invoice_id + ); + + $payment = $wpdb->get_row($query, ARRAY_A); + + if ($payment && isset($payment['metadata']) && $payment['metadata']) { + $payment['metadata'] = json_decode($payment['metadata'], true); + } + + return $payment; + } catch (Exception $e) { + $this->logger->log("Exception getting payment by invoice: " . $e->getMessage()); + return false; + } + } + + /** + * Get payment by order ID + * + * @param int $order_id Order ID + * @return array|false Payment data or false if not found + */ + public function get_payment_by_order($order_id) { + global $wpdb; + + try { + $query = $wpdb->prepare( + "SELECT * FROM {$this->table_name} WHERE order_id = %d", + $order_id + ); + + $payment = $wpdb->get_row($query, ARRAY_A); + + if ($payment && isset($payment['metadata']) && $payment['metadata']) { + $payment['metadata'] = json_decode($payment['metadata'], true); + } + + return $payment; + } catch (Exception $e) { + $this->logger->log("Exception getting payment by order: " . $e->getMessage()); + return false; + } + } + + /** + * Get pending payments + * + * @param int $minutes_old Get payments older than this many minutes + * @param int $max_minutes_old Get payments younger than this many minutes + * @return array Array of payment data + */ + public function get_pending_payments($minutes_old = 2, $max_minutes_old = 60) { + global $wpdb; + + try { + $query = $wpdb->prepare( + "SELECT * FROM {$this->table_name} + WHERE status = 'pending' + AND created_at < DATE_SUB(NOW(), INTERVAL %d MINUTE) + AND created_at > DATE_SUB(NOW(), INTERVAL %d MINUTE)", + $minutes_old, + $max_minutes_old + ); + + $payments = $wpdb->get_results($query, ARRAY_A); + + // Parse metadata JSON + foreach ($payments as &$payment) { + if (isset($payment['metadata']) && $payment['metadata']) { + $payment['metadata'] = json_decode($payment['metadata'], true); + } + } + + return $payments; + } catch (Exception $e) { + $this->logger->log("Exception getting pending payments: " . $e->getMessage()); + return array(); + } + } +} diff --git a/includes/class-breez-logger.php b/includes/class-breez-logger.php new file mode 100644 index 0000000..95de792 --- /dev/null +++ b/includes/class-breez-logger.php @@ -0,0 +1,183 @@ +debug = $debug; + $this->start_time = microtime(true); + + if ($this->debug) { + $this->log('Logger initialized', 'debug'); + } + } + + /** + * Get elapsed time since logger initialization + * + * @return float Elapsed time in seconds + */ + private function get_elapsed_time() { + return microtime(true) - $this->start_time; + } + + /** + * Format context data for logging + * + * @param array $context Additional context data + * @return string Formatted context string + */ + private function format_context($context = array()) { + $default_context = array( + 'timestamp' => date('Y-m-d H:i:s'), + 'elapsed' => sprintf('%.4f', $this->get_elapsed_time()), + 'memory' => sprintf('%.2fMB', memory_get_usage() / 1024 / 1024) + ); + + $context = array_merge($default_context, $context); + return json_encode($context); + } + + /** + * Log a message + * + * @param string $message Message to log + * @param string $level Log level (debug, info, warning, error) + */ + /** + * Log a message with context + * + * @param string $message Message to log + * @param string $level Log level (debug, info, warning, error) + * @param array $context Additional context data + */ + public function log($message, $level = 'info', $context = array()) { + if (!$this->debug && $level !== 'error') { + return; + } + + try { + // Add trace for errors + if ($level === 'error') { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + $caller = isset($trace[1]) ? $trace[1] : $trace[0]; + $context['file'] = isset($caller['file']) ? basename($caller['file']) : ''; + $context['line'] = isset($caller['line']) ? $caller['line'] : ''; + $context['function'] = isset($caller['function']) ? $caller['function'] : ''; + } + + $formatted_context = $this->format_context($context); + $log_message = sprintf('[%s] %s | %s', strtoupper($level), $message, $formatted_context); + + if (function_exists('wc_get_logger')) { + // Use WC_Logger if available + $logger = wc_get_logger(); + $logger_context = array_merge(array('source' => 'breez'), $context); + + switch ($level) { + case 'debug': + $logger->debug($log_message, $logger_context); + break; + case 'warning': + $logger->warning($log_message, $logger_context); + break; + case 'error': + $logger->error($log_message, $logger_context); + break; + case 'info': + default: + $logger->info($log_message, $logger_context); + break; + } + } else { + // Fallback to WP debug log if enabled + if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + error_log(sprintf( + '[Breez %s] %s', + strtoupper($level), + $message + )); + } + } + } catch (Exception $e) { + // Last resort fallback - only if WP_DEBUG is enabled + if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + error_log(sprintf( + '[Breez Logger Error] Failed to log message: %s. Original message: %s', + $e->getMessage(), + $message + )); + } + } + } + + /** + * Log API request + * + * @param string $endpoint API endpoint + * @param array $params Request parameters + */ + public function log_request($endpoint, $params = array()) { + $this->log( + "API Request to $endpoint", + 'debug', + array( + 'endpoint' => $endpoint, + 'params' => $params + ) + ); + } + + /** + * Log API response + * + * @param string $endpoint API endpoint + * @param mixed $response Response data + * @param int $status_code HTTP status code + */ + public function log_response($endpoint, $response, $status_code = null) { + $context = array( + 'endpoint' => $endpoint, + 'status_code' => $status_code, + 'response' => is_array($response) ? $response : array('raw' => substr((string)$response, 0, 500)) + ); + + $level = ($status_code && $status_code >= 400) ? 'error' : 'debug'; + $this->log( + "API Response from $endpoint", + $level, + $context + ); + } +} diff --git a/includes/class-breez-payment-handler.php b/includes/class-breez-payment-handler.php new file mode 100644 index 0000000..70174d9 --- /dev/null +++ b/includes/class-breez-payment-handler.php @@ -0,0 +1,389 @@ +logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); + $this->logger->log('Initializing payment handler', 'debug'); + + try { + // If no client is provided, create one + if (!$client) { + $api_url = get_option('woocommerce_breez_api_url'); + $api_key = get_option('woocommerce_breez_api_key'); + + if (!$api_url || !$api_key) { + throw new Exception('API credentials not configured'); + } + + $this->client = new Breez_API_Client($api_url, $api_key); + } else { + $this->client = $client; + } + + // Verify API connectivity + if (!$this->client->check_health()) { + throw new Exception('API health check failed'); + } + + // If no db_manager is provided, create one + if (!$db_manager) { + $this->db_manager = new Breez_DB_Manager(); + } else { + $this->db_manager = $db_manager; + } + + $this->logger->log('Payment handler initialized successfully', 'debug'); + + } catch (Exception $e) { + $this->logger->log('Payment handler initialization failed: ' . $e->getMessage(), 'error'); + throw $e; + } + } + + /** + * Convert fiat amount to satoshis + * + * @param float $amount Fiat amount + * @param string $currency Currency code (default: store currency) + * @return int Amount in satoshis + */ + public function convert_to_satoshis($amount, $currency = '') { + try { + if (!$currency) { + $currency = get_woocommerce_currency(); + } + + if ($amount <= 0) { + throw new Exception('Invalid amount: must be greater than 0'); + } + + $start_time = microtime(true); + + // Get exchange rate from BTC to currency + $btc_rate = $this->get_btc_rate($currency); + + if ($btc_rate <= 0) { + throw new Exception("Invalid exchange rate for $currency"); + } + + // Convert amount to BTC + $btc_amount = $amount / $btc_rate; + + // Convert BTC to satoshis (1 BTC = 100,000,000 satoshis) + $satoshis = round($btc_amount * 100000000); + + $duration = round(microtime(true) - $start_time, 3); + + $this->logger->log('Currency conversion completed', 'debug', array( + 'amount' => $amount, + 'currency' => $currency, + 'btc_rate' => $btc_rate, + 'satoshis' => $satoshis, + 'duration' => $duration + )); + + return $satoshis; + + } catch (Exception $e) { + $this->logger->log('Currency conversion failed: ' . $e->getMessage(), 'error'); + throw $e; + } + } + + /** + * Get Bitcoin exchange rate for currency + * + * In a real implementation, this would call an exchange rate API + * + * @param string $currency Currency code + * @return float Exchange rate (1 BTC = X currency) + */ + public function get_btc_rate($currency = '') { + if (!$currency) { + $currency = get_woocommerce_currency(); + } + + // Try to get from transient cache first (valid for 10 minutes) + $cached_rate = get_transient('breez_btc_' . strtolower($currency) . '_rate'); + if ($cached_rate !== false) { + $this->logger->log('Using cached exchange rate', 'debug', array( + 'currency' => $currency, + 'rate' => $cached_rate + )); + return $cached_rate; + } + + try { + // Get rate from CoinGecko API + $response = wp_remote_get( + 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=' . strtolower($currency) + ); + + if (is_wp_error($response)) { + throw new Exception('Failed to fetch exchange rate: ' . $response->get_error_message()); + } + + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (!isset($body['bitcoin'][strtolower($currency)])) { + throw new Exception('Invalid response from exchange rate API'); + } + + $rate = floatval($body['bitcoin'][strtolower($currency)]); + + // Add exchange rate buffer if configured + $buffer_percent = floatval(get_option('woocommerce_breez_exchange_rate_buffer', 1.0)); + if ($buffer_percent > 0) { + $rate = $rate * (1 + ($buffer_percent / 100)); + } + + // Cache for 10 minutes + set_transient('breez_btc_' . strtolower($currency) . '_rate', $rate, 10 * MINUTE_IN_SECONDS); + + $this->logger->log('Fetched new exchange rate', 'debug', array( + 'currency' => $currency, + 'rate' => $rate, + 'buffer' => $buffer_percent . '%' + )); + + return $rate; + + } catch (Exception $e) { + $this->logger->log('Exchange rate fetch failed: ' . $e->getMessage(), 'error'); + + // Fallback rates if API fails + $default_rates = array( + 'USD' => 50000.00, + 'EUR' => 45000.00, + 'GBP' => 40000.00, + ); + + $rate = isset($default_rates[$currency]) ? $default_rates[$currency] : $default_rates['USD']; + + $this->logger->log('Using fallback exchange rate', 'warning', array( + 'currency' => $currency, + 'rate' => $rate + )); + + return $rate; + } + } + + /** + * Check payment status + * + * @param string $invoice_id Invoice ID + * @return string Payment status (pending, completed, failed) + */ + public function check_payment_status($invoice_id) { + try { + $start_time = microtime(true); + $this->logger->log("Checking payment status", 'debug', array('invoice_id' => $invoice_id)); + + if (!$invoice_id) { + throw new Exception('Invalid invoice ID'); + } + + // First check the local database + $payment = $this->db_manager->get_payment_by_invoice($invoice_id); + + if ($payment && $payment['status'] !== 'pending') { + $this->logger->log('Using cached payment status', 'debug', array( + 'invoice_id' => $invoice_id, + 'status' => $payment['status'] + )); + return $payment['status']; // Return cached status if not pending + } + + + // If pending or not found, check with API + $status = $this->client->check_payment_status($invoice_id); + + // Update database if status has changed and we have payment data + if ($payment && $status !== 'pending' && $status !== $payment['status']) { + $this->db_manager->update_payment_status($payment['order_id'], $status); + + $this->logger->log('Payment status updated', 'info', array( + 'invoice_id' => $invoice_id, + 'old_status' => $payment['status'], + 'new_status' => $status, + 'order_id' => $payment['order_id'] + )); + } + + $duration = round(microtime(true) - $start_time, 3); + $this->logger->log('Payment status check completed', 'debug', array( + 'invoice_id' => $invoice_id, + 'status' => $status, + 'duration' => $duration + )); + + return $status; + + } catch (Exception $e) { + $this->logger->log('Payment status check failed: ' . $e->getMessage(), 'error', array( + 'invoice_id' => $invoice_id + )); + throw $e; + } + } + + /** + * Process successful payment + * + * @param string $invoice_id Invoice ID + * @return bool Success/failure + */ + public function process_successful_payment($invoice_id) { + $payment = $this->db_manager->get_payment_by_invoice($invoice_id); + + if (!$payment) { + $this->logger->log("No payment found for invoice $invoice_id"); + return false; + } + + $order_id = $payment['order_id']; + $order = wc_get_order($order_id); + + if (!$order) { + $this->logger->log("No order found for order ID $order_id"); + return false; + } + + // Update payment status in database + $this->db_manager->update_payment_status($order_id, 'completed'); + + // Update order status if not already completed + if (!$order->has_status(array('processing', 'completed'))) { + $order->payment_complete($invoice_id); + $order->add_order_note(__('Payment completed via Breez.', 'breez-woocommerce')); + $this->logger->log("Payment completed for order #$order_id (invoice: $invoice_id)"); + } + + return true; + } + + /** + * Process failed payment + * + * @param string $invoice_id Invoice ID + * @return bool Success/failure + */ + public function process_failed_payment($invoice_id) { + $payment = $this->db_manager->get_payment_by_invoice($invoice_id); + + if (!$payment) { + $this->logger->log("No payment found for invoice $invoice_id"); + return false; + } + + $order_id = $payment['order_id']; + $order = wc_get_order($order_id); + + if (!$order) { + $this->logger->log("No order found for order ID $order_id"); + return false; + } + + // Update payment status in database + $this->db_manager->update_payment_status($order_id, 'failed'); + + // Update order status if still pending + if ($order->has_status('pending')) { + $order->update_status('failed', __('Payment failed or expired.', 'breez-woocommerce')); + $this->logger->log("Payment failed for order #$order_id (invoice: $invoice_id)"); + } + + return true; + } + + /** + * Check pending payments (called by cron job) + */ + public function check_pending_payments() { + $this->logger->log("Checking pending payments..."); + + // Get pending payments that are at least 2 minutes old (to avoid race conditions) + // but less than the expiry time (default: 60 minutes) + $pending_payments = $this->db_manager->get_pending_payments(2, 60); + + if (empty($pending_payments)) { + $this->logger->log("No pending payments to check"); + return; + } + + $this->logger->log("Found " . count($pending_payments) . " pending payments to check"); + + foreach ($pending_payments as $payment) { + $invoice_id = $payment['invoice_id']; + $order_id = $payment['order_id']; + + $this->logger->log("Checking payment status for invoice $invoice_id (order #$order_id)"); + + // Check payment status with API + $status = $this->client->check_payment_status($invoice_id); + + if ($status === 'completed') { + $this->process_successful_payment($invoice_id); + } elseif ($status === 'failed') { + $this->process_failed_payment($invoice_id); + } else { + // Check if payment is expired + $order = wc_get_order($order_id); + + if ($order) { + $expiry = $order->get_meta('_breez_payment_expiry'); + $current_time = time(); + + if ($expiry && $current_time > $expiry) { + $this->logger->log("Payment expired for order #$order_id (invoice: $invoice_id)"); + $this->process_failed_payment($invoice_id); + } + } + } + } + + $this->logger->log("Finished checking pending payments"); + } +} diff --git a/includes/class-breez-webhook-handler.php b/includes/class-breez-webhook-handler.php new file mode 100644 index 0000000..d2f42a0 --- /dev/null +++ b/includes/class-breez-webhook-handler.php @@ -0,0 +1,125 @@ +log("Received webhook request"); + + // Get request data + $data = $request->get_json_params(); + + if (!$data) { + self::$logger->log("Invalid webhook data: empty or not JSON"); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Invalid request data' + ), 400); + } + + self::$logger->log("Webhook data: " . json_encode($data)); + + // Check for required fields + if (!isset($data['invoice_id']) || !isset($data['status'])) { + self::$logger->log("Missing required fields in webhook data"); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Missing required fields' + ), 400); + } + + $invoice_id = $data['invoice_id']; + $status = $data['status']; + + // Process the payment + $db_manager = new Breez_DB_Manager(); + $payment = $db_manager->get_payment_by_invoice($invoice_id); + + if (!$payment) { + self::$logger->log("No payment found for invoice $invoice_id"); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Payment not found' + ), 404); + } + + $order_id = $payment['order_id']; + $order = wc_get_order($order_id); + + if (!$order) { + self::$logger->log("No order found for order ID $order_id"); + return new WP_REST_Response(array( + 'success' => false, + 'message' => 'Order not found' + ), 404); + } + + $payment_handler = new Breez_Payment_Handler(null, $db_manager); + + // Update payment status based on the webhook data + if ($status === 'SUCCEEDED') { + $payment_handler->process_successful_payment($invoice_id); + } elseif ($status === 'FAILED') { + $payment_handler->process_failed_payment($invoice_id); + } else { + self::$logger->log("Unknown payment status: $status"); + } + + return new WP_REST_Response(array( + 'success' => true, + 'message' => 'Webhook processed successfully' + ), 200); + } +} diff --git a/templates/admin-settings.php b/templates/admin-settings.php new file mode 100644 index 0000000..069112c --- /dev/null +++ b/templates/admin-settings.php @@ -0,0 +1,50 @@ + + +
+

+ +

+ +

+
    +
  • +
  • +
  • +
+ +

+

+
+ + diff --git a/templates/payment-instructions.php b/templates/payment-instructions.php new file mode 100644 index 0000000..0c8b8dc --- /dev/null +++ b/templates/payment-instructions.php @@ -0,0 +1,463 @@ + + +
+

+ + +
+

+

+
+ +
+

+

+
+ + 0): ?> +
+

+
+ +
+
+
+

+
+
+ +
+

+
+ + +
+ +

+ +

+
+ +
+

+
+ + +
+ + + +