diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..9578c65 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,6 @@ +.btcpay-connection-success { + color: green; +} +.btcpay-connection-error { + color: red; +} diff --git a/assets/js/apiKeyRedirect.js b/assets/js/apiKeyRedirect.js index ebca6a5..68ce577 100644 --- a/assets/js/apiKeyRedirect.js +++ b/assets/js/apiKeyRedirect.js @@ -32,4 +32,23 @@ jQuery(document).ready(function($) { alert('Please enter a valid url including https:// in the BTCPay Server URL input field.') } }); + + // Handle manual connection settings. + const showDetails = $('#btcpay_gf_connection_details'); + const detailFields = $('#btcpay_gf_store_id, #btcpay_gf_whsecret, #btcpay_gf_api_key, #btcpay_gf_whstatus'); + + toggleFields(showDetails.is(':checked')); + + showDetails.on('change', function() { + toggleFields($(this).is(':checked')); + }); + + function toggleFields(isChecked) { + if (isChecked) { + detailFields.closest('tr').show(); + } else { + detailFields.closest('tr').hide(); + } + } + }); diff --git a/btcpay-greenfield-for-woocommerce.php b/btcpay-greenfield-for-woocommerce.php index 8dd8a61..722407d 100644 --- a/btcpay-greenfield-for-woocommerce.php +++ b/btcpay-greenfield-for-woocommerce.php @@ -7,12 +7,12 @@ * Author URI: https://btcpayserver.org * Text Domain: btcpay-greenfield-for-woocommerce * Domain Path: /languages - * Version: 2.1.0 + * Version: 2.2.0 * Requires PHP: 7.4 - * Tested up to: 6.2 + * Tested up to: 6.3 * Requires at least: 5.2 * WC requires at least: 6.0 - * WC tested up to: 7.5 + * WC tested up to: 8.0 */ use BTCPayServer\WC\Admin\Notice; @@ -25,7 +25,7 @@ defined( 'ABSPATH' ) || exit(); -define( 'BTCPAYSERVER_VERSION', '2.1.0' ); +define( 'BTCPAYSERVER_VERSION', '2.2.0' ); define( 'BTCPAYSERVER_VERSION_KEY', 'btcpay_gf_version' ); define( 'BTCPAYSERVER_PLUGIN_FILE_PATH', plugin_dir_path( __FILE__ ) ); define( 'BTCPAYSERVER_PLUGIN_URL', plugin_dir_url(__FILE__ ) ); @@ -373,6 +373,7 @@ function init_btcpay_greenfield() { if ($apiData->hasSingleStore() && $apiData->hasRequiredPermissions()) { update_option('btcpay_gf_api_key', $apiData->getApiKey()); update_option('btcpay_gf_store_id', $apiData->getStoreID()); + update_option('btcpay_gf_connection_details', 'yes'); Notice::addNotice('success', __('Successfully received api key and store id from BTCPay Server API. Please finish setup by saving this settings form.', 'btcpay-greenfield-for-woocommerce')); wp_redirect($btcPaySettingsUrl); } else { diff --git a/readme.txt b/readme.txt index 407e351..63fd957 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: ndeet, kukks, nicolasdorier Donate link: https://btcpayserver.org/donate/ Tags: bitcoin, btcpay, BTCPay Server, btcpayserver, WooCommerce, payment gateway, accept bitcoin, bitcoin plugin, bitcoin payment processor, bitcoin e-commerce, Lightning Network, Litecoin, cryptocurrency Requires at least: 5.2 -Tested up to: 6.2 +Tested up to: 6.3 Requires PHP: 7.4 -Stable tag: 2.1.0 +Stable tag: 2.2.0 License: MIT License URI: https://github.com/btcpayserver/woocommerce-greenfield-plugin/blob/master/license.txt diff --git a/src/Admin/GlobalSettings.php b/src/Admin/GlobalSettings.php index 3b1ce92..5e7191f 100644 --- a/src/Admin/GlobalSettings.php +++ b/src/Admin/GlobalSettings.php @@ -17,13 +17,15 @@ * todo: add validation of host/url */ class GlobalSettings extends \WC_Settings_Page { - + private GreenfieldApiHelper $apiHelper; public function __construct() { $this->id = 'btcpay_settings'; $this->label = __( 'BTCPay Settings', 'btcpay-greenfield-for-woocommerce' ); + $this->apiHelper = new GreenfieldApiHelper(); // Register custom field type order_states with OrderStatesField class. add_action('woocommerce_admin_field_order_states', [(new OrderStates()), 'renderOrderStatesHtml']); + add_action('woocommerce_admin_field_custom_markup', [$this, 'output_custom_markup_field']); if (is_admin()) { // Register and include JS. @@ -34,13 +36,20 @@ public function __construct() [ 'url' => admin_url( 'admin-ajax.php' ), 'apiNonce' => wp_create_nonce( 'btcpaygf-api-url-nonce' ), - ]); + ] + ); + + // Register and include CSS. + wp_register_style( 'btcpay_gf_admin_styles', BTCPAYSERVER_PLUGIN_URL . 'assets/css/admin.css', array(), BTCPAYSERVER_VERSION ); + wp_enqueue_style( 'btcpay_gf_admin_styles' ); + } parent::__construct(); } public function output(): void { + echo '

' . _x('BTCPay Server Payments settings', 'global_settings', 'btcpay-greenfield-for-woocommerce') . '

'; $settings = $this->get_settings_for_default_section(); \WC_Admin_Settings::output_fields($settings); } @@ -53,16 +62,44 @@ public function get_settings_for_default_section(): array public function getGlobalSettings(): array { Logger::debug('Entering Global Settings form.'); + + // Check setup status and prepare output. + $setupStatus = ''; + if ($this->apiHelper->configured) { + $setupStatus = '

' . _x('BTCPay Server connected.', 'global_settings', 'btcpay-greenfield-for-woocommerce') . '

'; + } else { + $setupStatus = '

' . _x('Not connected. Please use the setup wizard above or check advanced settings to manually enter connection settings.', 'global_settings', 'btcpay-greenfield-for-woocommerce') . '

'; + } + + // Check webhook status and prepare output. + $whStatus = ''; + $whId = ''; + // Can't use apiHelper because of caching. + if ($webhookConfig = get_option('btcpay_gf_webhook')) { + $whId = $webhookConfig['id']; + } + + if ($this->apiHelper->webhookIsSetup()) { + $whStatus = '

' . _x('Webhook setup automatically.', 'global_settings', 'btcpay-greenfield-for-woocommerce') . ' ID: ' . $whId . '

'; + } else { + $whStatus = '

' . _x('No webhook setup, yet.', 'global_settings', 'btcpay-greenfield-for-woocommerce') . '

'; + } + + if ($this->apiHelper->webhookIsSetupManual()) { + $whStatus = '

' . _x('Webhook setup manually with webhook secret.', 'global_settings', 'btcpay-greenfield-for-woocommerce') . ' ID: ' . $whId . '

'; + } + return [ - 'title' => [ + // Section connection. + 'title_connection' => [ 'title' => esc_html_x( - 'BTCPay Server Payments Settings', + 'Connection settings', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'title', 'desc' => sprintf( _x( 'This plugin version is %s and your PHP version is %s. Check out our installation instructions. If you need assistance, please come on our chat. Thank you for using BTCPay!', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), BTCPAYSERVER_VERSION, PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION ), - 'id' => 'btcpay_gf' + 'id' => 'btcpay_gf_connection' ], 'url' => [ 'title' => esc_html_x( @@ -76,10 +113,29 @@ public function getGlobalSettings(): array 'desc_tip' => true, 'id' => 'btcpay_gf_url' ], + 'wizard' => [ + 'title' => esc_html_x( 'Setup wizard', 'global_settings','btcpay-greenfield-for-woocommerce' ), + 'type' => 'custom_markup', + 'markup' => '', + 'id' => 'btcpay_gf_wizard_button' // a unique ID + ], + 'status' => [ + 'title' => esc_html_x( 'Setup status', 'global_settings','btcpay-greenfield-for-woocommerce' ), + 'type' => 'custom_markup', + 'markup' => $setupStatus, + 'id' => 'btcpay_gf_status' + ], + 'connection_details' => [ + 'title' => __( 'Advanced settings', 'btcpay-greenfield-for-woocommerce' ), + 'type' => 'checkbox', + 'default' => 'no', + 'desc' => _x( 'Show all connection settings / manual setup.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), + 'id' => 'btcpay_gf_connection_details' + ], 'api_key' => [ 'title' => esc_html_x( 'BTCPay API Key', 'global_settings','btcpay-greenfield-for-woocommerce' ), 'type' => 'text', - 'desc' => _x( 'Your BTCPay API Key. If you do not have any yet click here to generate API keys.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), + 'desc' => _x( 'Your BTCPay API Key. If you do not have any yet use the setup wizard above.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'default' => '', 'id' => 'btcpay_gf_api_key' ], @@ -90,6 +146,33 @@ public function getGlobalSettings(): array 'default' => '', 'id' => 'btcpay_gf_store_id' ], + 'whsecret' => [ + 'title' => esc_html_x( 'Webhook secret (optional)', 'global_settings','btcpay-greenfield-for-woocommerce' ), + 'type' => 'text', + 'desc' => _x( 'If left empty an webhook will created automatically on save. Only fill out if you know the webhook secret and the webhook was created manually on BTCPay Server.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), + 'default' => '', + 'id' => 'btcpay_gf_whsecret' + ], + 'whstatus' => [ + 'title' => esc_html_x( 'Webhook status', 'global_settings','btcpay-greenfield-for-woocommerce' ), + 'type' => 'custom_markup', + 'markup' => $whStatus, + 'id' => 'btcpay_gf_whstatus' + ], + 'sectionend_connection' => [ + 'type' => 'sectionend', + 'id' => 'btcpay_gf_connection', + ], + // Section general. + 'title' => [ + 'title' => esc_html_x( + 'General settings', + 'global_settings', + 'btcpay-greenfield-for-woocommerce' + ), + 'type' => 'title', + 'id' => 'btcpay_gf' + ], 'default_description' => [ 'title' => esc_html_x( 'Default Customer Message', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'textarea', @@ -170,6 +253,7 @@ public function save() { $apiUrl = esc_url_raw( $_POST['btcpay_gf_url'] ); $apiKey = sanitize_text_field( $_POST['btcpay_gf_api_key'] ); $storeId = sanitize_text_field( $_POST['btcpay_gf_store_id'] ); + $manualWhSecret = sanitize_text_field( $_POST['btcpay_gf_whsecret'] ); // todo: fix change of url + key + storeid not leading to recreation of webhook. // Check if the provided API key has the right scope and permissions. @@ -227,20 +311,58 @@ public function save() { // Continue creating the webhook if the API key permissions are OK. if ( false === $hasError ) { // Check if we already have a webhook registered for that store. - if ( GreenfieldApiWebhook::webhookExists( $apiUrl, $apiKey, $storeId ) ) { - $messageReuseWebhook = __( 'Webhook already exists, skipping webhook creation.', 'btcpay-greenfield-for-woocommerce' ); - Notice::addNotice('info', $messageReuseWebhook, true); - Logger::debug($messageReuseWebhook); - } else { - // Register a new webhook. - if ( GreenfieldApiWebhook::registerWebhook( $apiUrl, $apiKey, $storeId ) ) { - $messageWebhookSuccess = __( 'Successfully registered a new webhook on BTCPay Server.', 'btcpay-greenfield-for-woocommerce' ); - Notice::addNotice('success', $messageWebhookSuccess, true ); - Logger::debug( $messageWebhookSuccess ); + if ( GreenfieldApiWebhook::webhookExists( $apiUrl, $apiKey, $storeId, $manualWhSecret ) ) { + + if ( $manualWhSecret && $this->apiHelper->webhook['secret'] !== $manualWhSecret) { + // Store manual webhook in options table. + update_option( + 'btcpay_gf_webhook', + [ + 'id' => 'manual', + 'secret' => $manualWhSecret, + 'url' => 'manual' + ] + ); + + $messageWebhookManual = __( 'Successfully setup manual webhook.', 'btcpay-greenfield-for-woocommerce' ); + Notice::addNotice('success', $messageWebhookManual, true ); + Logger::debug( $messageWebhookManual ); } else { - $messageWebhookError = __( 'Could not register a new webhook on the store.', 'btcpay-greenfield-for-woocommerce' ); - Notice::addNotice('error', $messageWebhookError ); - Logger::debug($messageWebhookError, true); + $messageReuseWebhook = __( 'Webhook already exists, skipping webhook creation.', 'btcpay-greenfield-for-woocommerce' ); + Notice::addNotice('info', $messageReuseWebhook, true); + Logger::debug($messageReuseWebhook); + } + } else { + // When the webhook secret was set manually we just store it and not try to create it. + if ( $manualWhSecret ) { + // Store manual webhook in options table. + update_option( + 'btcpay_gf_webhook', + [ + 'id' => 'manual', + 'secret' => $manualWhSecret, + 'url' => 'manual' + ] + ); + + $messageWebhookManual = __( 'Successfully setup manual webhook.', 'btcpay-greenfield-for-woocommerce' ); + Notice::addNotice('success', $messageWebhookManual, true ); + Logger::debug( $messageWebhookManual ); + } + + // Register a new webhook automatically. + if ( empty($manualWhSecret) ) { + if ( GreenfieldApiWebhook::registerWebhook( $apiUrl, $apiKey, $storeId ) ) { + $messageWebhookSuccess = __( 'Successfully registered a new webhook on BTCPay Server.', 'btcpay-greenfield-for-woocommerce' ); + Notice::addNotice('success', $messageWebhookSuccess, true ); + Logger::debug( $messageWebhookSuccess ); + } else { + $messageWebhookError = __( 'Could not register a new webhook on the store.', 'btcpay-greenfield-for-woocommerce' ); + Notice::addNotice('error', $messageWebhookError ); + Logger::debug($messageWebhookError, true); + // Cleanup existing conf. + delete_option('btcpay_gf_webhook'); + } } } @@ -293,4 +415,19 @@ private function hasNeededApiCredentials(): bool { } return false; } + + public function output_custom_markup_field($value) { + echo ''; + if (!empty($value['title'])) { + echo '' . esc_html($value['title']) . ''; + } else { + echo ' '; + } + + echo ''; + echo $value['markup']; + echo ''; + echo ''; + } + } diff --git a/src/Helper/GreenfieldApiAuthorization.php b/src/Helper/GreenfieldApiAuthorization.php index 95cd075..e670851 100644 --- a/src/Helper/GreenfieldApiAuthorization.php +++ b/src/Helper/GreenfieldApiAuthorization.php @@ -8,12 +8,12 @@ class GreenfieldApiAuthorization { public const REQUIRED_PERMISSIONS = [ 'btcpay.store.canviewinvoices', 'btcpay.store.cancreateinvoice', - 'btcpay.store.webhooks.canmodifywebhooks', 'btcpay.store.canviewstoresettings', 'btcpay.store.canmodifyinvoices' ]; public const OPTIONAL_PERMISSIONS = [ - 'btcpay.store.cancreatenonapprovedpullpayments' + 'btcpay.store.cancreatenonapprovedpullpayments', + 'btcpay.store.webhooks.canmodifywebhooks', ]; private $apiKey; @@ -83,4 +83,12 @@ public function hasRefundsPermission(): bool { return in_array('btcpay.store.cancreatenonapprovedpullpayments', $permissions, true); } + + public function hasWebhookPermission(): bool { + $permissions = array_reduce($this->permissions, static function (array $carry, string $permission) { + return array_merge($carry, [explode(':', $permission)[0]]); + }, []); + + return in_array('btcpay.store.webhooks.canmodifywebhooks', $permissions, true); + } } diff --git a/src/Helper/GreenfieldApiHelper.php b/src/Helper/GreenfieldApiHelper.php index 3b89647..3f5df05 100644 --- a/src/Helper/GreenfieldApiHelper.php +++ b/src/Helper/GreenfieldApiHelper.php @@ -161,6 +161,24 @@ public static function apiCredentialsExist(string $apiUrl, string $apiKey, strin return false; } + public static function webhookIsSetup(): bool { + if ($config = self::getConfig()) { + return !empty($config['webhook']['secret']); + } + + return false; + } + + public static function webhookIsSetupManual(): bool { + if ($config = self::getConfig()) { + return !empty($config['webhook']['secret']) && $config['webhook']['id'] === 'manual'; + } + + return false; + } + + + /** * Checks if a given invoice id has status of fully paid (settled) or paid late. */ diff --git a/src/Helper/GreenfieldApiWebhook.php b/src/Helper/GreenfieldApiWebhook.php index 5a2ee98..3e8120a 100644 --- a/src/Helper/GreenfieldApiWebhook.php +++ b/src/Helper/GreenfieldApiWebhook.php @@ -20,8 +20,16 @@ class GreenfieldApiWebhook { /** * Get locally stored webhook data and check if it exists on the store. */ - public static function webhookExists(string $apiUrl, string $apiKey, string $storeId): bool { + public static function webhookExists(string $apiUrl, string $apiKey, string $storeId, $manualWebhookSecret = null): bool { + if ( $storedWebhook = get_option( 'btcpay_gf_webhook' ) ) { + // Handle case of manually entered webhook (secret). We can't query webhooks endpoint at all without permission. + if ($storedWebhook['id'] === 'manual' && $storedWebhook['secret'] === $manualWebhookSecret) { + Logger::debug('Detected existing and manually set webhook.'); + return true; + } + + // Check automatically created webhook. try { $whClient = new Webhook( $apiUrl, $apiKey ); $existingWebhook = $whClient->getWebhook( $storeId, $storedWebhook['id'] ); @@ -30,6 +38,7 @@ public static function webhookExists(string $apiUrl, string $apiKey, string $sto $existingWebhook->getData()['id'] === $storedWebhook['id'] && strpos( $existingWebhook->getData()['url'], $storedWebhook['url'] ) !== false ) { + Logger::debug('Detected existing automatically set webhook.'); return true; } } catch (\Throwable $e) {