From 46eee0c9719de5eb8d6a8bee66af523bb7b417f0 Mon Sep 17 00:00:00 2001 From: ndeet Date: Fri, 31 Mar 2023 14:01:43 +0200 Subject: [PATCH] Add modal checkout mode. --- assets/js/modalCheckout.js | 188 ++++++++++++++++++++++++++ btcpay-greenfield-for-woocommerce.php | 27 ++++ src/Admin/GlobalSettings.php | 35 +++-- src/Gateway/AbstractGateway.php | 63 ++++++++- 4 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 assets/js/modalCheckout.js diff --git a/assets/js/modalCheckout.js b/assets/js/modalCheckout.js new file mode 100644 index 0000000..192807c --- /dev/null +++ b/assets/js/modalCheckout.js @@ -0,0 +1,188 @@ +jQuery(function ($) { + /** + * Main entry point. + */ + // Listen on Update cart and change of payment methods. + $('body').on('init_checkout updated_checkout payment_method_selected', function (event) { + if (BTCPayWP.modalEnabled == 1) { + btcpaySelected(); + } + }); + + /** + * Trigger ajax request to create order object and assign an invoice id. + */ + var processOrder = function () { + + let responseData = null; + + // Block the UI. + blockElement('.woocommerce-checkout-payment'); + + // Prepare form data and additional required data. + let formData = $('form.checkout').serialize(); + let additionalData = { + 'action': 'btcpaygf_modal_checkout', + 'apiNonce': BTCPayWP.apiNonce, + }; + + let data = $.param(additionalData) + '&' + formData; + + // We need to make sure the order processing worked before returning from this function. + $.ajaxSetup({async: false}); + + $.post(wc_checkout_params.checkout_url, data, function (response) { + //console.log('Received response when processing order: '); + //console.log(response); + + if (response.invoiceId) { + responseData = response; + } else { + unblockElement('.woocommerce-checkout-payment'); + // Show errors. + if (response.messages) { + submitError(response.messages); + } else { + submitError('
' + wc_checkout_params.i18n_checkout_error + '
'); // eslint-disable-line max-len + } + } + }).fail(function () { + unblockElement('.woocommerce-checkout-payment'); + submitError('
' + wc_checkout_params.i18n_checkout_error + '
'); + }); + + // Reenable async. + $.ajaxSetup({async: true}); + + return responseData; + }; + + /** + * Show the BTCPay modal and listen to events sent by BTCPay server. + */ + var showBTCPayModal = function(data) { + //console.log('Triggered showBTCPayModal()'); + + if (data.invoiceId !== undefined) { + window.btcpay.setApiUrlPrefix(BTCPayWP.apiUrl); + window.btcpay.showInvoice(data.invoiceId); + } + let invoice_paid = false; + window.btcpay.onModalReceiveMessage(function (event) { + if (isObject(event.data)) { + //console.log('BTCPay modal event: invoiceId: ' + event.data.invoiceId); + //console.log('BTCPay modal event: status: ' + event.data.status); + if (event.data.status) { + switch (event.data.status) { + case 'complete': + case 'paid': + invoice_paid = true; + window.location = data.orderCompleteLink; + break; + case 'expired': + window.btcpay.hideFrame(); + submitError(BTCPayWP.textInvoiceExpired); + break; + } + } + } else { // handle event.data "loaded" "closed" + if (event.data === 'close') { + if (invoice_paid === true) { + window.location = data.orderCompleteLink; + } + submitError(BTCPayWP.textModalClosed); + } + } + }); + const isObject = obj => { + return Object.prototype.toString.call(obj) === '[object Object]' + } + } + + /** + * Block UI of a given element. + */ + var blockElement = function (cssClass) { + //console.log('Triggered blockElement.'); + + $(cssClass).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + }; + + /** + * Unblock UI of a given element. + */ + var unblockElement = function (cssClass) { + //console.log('Triggered unblockElement.'); + $(cssClass).unblock(); + }; + + /** + * Show errors, mostly copied from WC checkout.js + * + * @param error_message + */ + var submitError = function (error_message) { + let $checkoutForm = $('form.checkout'); + $('.woocommerce-NoticeGroup-checkout, .woocommerce-error, .woocommerce-message').remove(); + $checkoutForm.prepend('
' + error_message + '
'); // eslint-disable-line max-len + $checkoutForm.removeClass('processing').unblock(); + $checkoutForm.find('.input-text, select, input:checkbox').trigger('validate').trigger('blur'); + scrollToNotices(); + $(document.body).trigger('checkout_error', [error_message]); + unblockElement('.woocommerce-checkout-payment'); + }; + + /** + * Scroll to errors on top of form, copied from WC checkout.js. + */ + var scrollToNotices = function () { + var scrollElement = $('.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout'); + + if (!scrollElement.length) { + scrollElement = $('form.checkout'); + } + + $.scroll_to_notices(scrollElement); + }; + + /** + * Trigger payframe button submit. + */ + var submitOrder = function (e) { + e.preventDefault(); + //console.log('Triggered submitOrder'); + + let responseData = processOrder(); + if (responseData) { + //console.log('Got invoice, opening modal.'); + blockElement('.woocommerce-checkout-payment'); + showBTCPayModal(responseData); + } + + return false; + }; + + /** + * Makes sure to trigger on payment method changes and overriding the default button submit handler. + */ + var btcpaySelected = function () { + var checkout_form = $('form.woocommerce-checkout'); + var selected_gateway = $('form[name="checkout"] input[name="payment_method"]:checked').val(); + unblockElement('.woocommerce-checkout-payment'); + + if (selected_gateway.startsWith('btcpaygf_')) { + // Bind our custom event handler to the checkout button. + checkout_form.on('checkout_place_order', submitOrder); + } else { + // Unbind custom event handlers. + checkout_form.off('checkout_place_order', submitOrder); + } + } + +}); diff --git a/btcpay-greenfield-for-woocommerce.php b/btcpay-greenfield-for-woocommerce.php index 4047070..4fdb530 100644 --- a/btcpay-greenfield-for-woocommerce.php +++ b/btcpay-greenfield-for-woocommerce.php @@ -39,6 +39,8 @@ public function __construct() { $this->includes(); add_action('woocommerce_thankyou_btcpaygf_default', ['BTCPayServerWCPlugin', 'orderStatusThankYouPage'], 10, 1); + add_action( 'wp_ajax_btcpaygf_modal_checkout', [$this, 'processAjaxModalCheckout'] ); + add_action( 'wp_ajax_nopriv_btcpaygf_modal_checkout', [$this, 'processAjaxModalCheckout'] ); // Run the updates. \BTCPayServer\WC\Helper\UpdateManager::processUpdates(); @@ -204,6 +206,31 @@ public function processAjaxApiUrl() { wp_send_json_error("Error processing Ajax request."); } + /** + * Handles the AJAX callback from the Payment Request on the checkout page. + */ + public function processAjaxModalCheckout() { + + Logger::debug('Entering ' . __METHOD__); + + $nonce = $_POST['apiNonce']; + if ( ! wp_verify_nonce( $nonce, 'btcpay-nonce' ) ) { + wp_die('Unauthorized!', '', ['response' => 401]); + } + + if ( get_option('btcpay_gf_modal_checkout') !== 'yes' ) { + wp_die('Modal checkout mode not enabled.', '', ['response' => 400]); + } + + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); + + try { + WC()->checkout()->process_checkout(); + } catch (\Throwable $e) { + Logger::debug('Error processing modal checkout ajax callback: ' . $e->getMessage()); + } + } + public static function orderStatusThankYouPage($order_id) { if (!$order = wc_get_order($order_id)) { diff --git a/src/Admin/GlobalSettings.php b/src/Admin/GlobalSettings.php index 82718e5..3b1ce92 100644 --- a/src/Admin/GlobalSettings.php +++ b/src/Admin/GlobalSettings.php @@ -54,43 +54,43 @@ public function getGlobalSettings(): array { Logger::debug('Entering Global Settings form.'); return [ - 'title' => [ + 'title' => [ 'title' => esc_html_x( 'BTCPay Server Payments Settings', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), - 'type' => 'title', + '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' ], - 'url' => [ - 'title' => esc_html_x( + 'url' => [ + 'title' => esc_html_x( 'BTCPay Server URL', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), - 'type' => 'text', + 'type' => 'text', 'desc' => esc_html_x( 'URL/host to your BTCPay Server instance. Note: if you use a self hosted node like Umbrel, RaspiBlitz, myNode, etc. you will have to make sure your node is reachable from the internet. You can do that through Tor, Cloudflare or SSH (advanced).', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'placeholder' => esc_attr_x( 'e.g. https://btcpayserver.example.com', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'desc_tip' => true, 'id' => 'btcpay_gf_url' ], - 'api_key' => [ + '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' ), 'default' => '', 'id' => 'btcpay_gf_api_key' ], - 'store_id' => [ + 'store_id' => [ 'title' => esc_html_x( 'Store ID', 'global_settings','btcpay-greenfield-for-woocommerce' ), 'type' => 'text', 'desc_tip' => _x( 'Your BTCPay Store ID. You can find it on the store settings page on your BTCPay Server.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'default' => '', 'id' => 'btcpay_gf_store_id' ], - 'default_description' => [ + 'default_description' => [ 'title' => esc_html_x( 'Default Customer Message', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'textarea', 'desc' => esc_html_x( 'Message to explain how the customer will be paying for the purchase. Can be overwritten on a per gateway basis.', 'btcpay-greenfield-for-woocommerce' ), @@ -98,7 +98,7 @@ public function getGlobalSettings(): array 'desc_tip' => true, 'id' => 'btcpay_gf_default_description' ], - 'transaction_speed' => [ + 'transaction_speed' => [ 'title' => esc_html_x( 'Invoice pass to "settled" state after', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'select', 'desc' => esc_html_x('An invoice becomes settled after the payment has this many confirmations...', 'global_settings', 'btcpay-greenfield-for-woocommerce'), @@ -113,32 +113,39 @@ public function getGlobalSettings(): array 'desc_tip' => true, 'id' => 'btcpay_gf_transaction_speed' ], - 'order_states' => [ + 'order_states' => [ 'type' => 'order_states', 'id' => 'btcpay_gf_order_states' ], - 'separate_gateways' => [ + 'modal_checkout' => [ + 'title' => __( 'Modal checkout', 'btcpay-greenfield-for-woocommerce' ), + 'type' => 'checkbox', + 'default' => 'no', + 'desc' => _x( 'Opens a modal overlay on the checkout page instead of redirecting to BTCPay Server.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), + 'id' => 'btcpay_gf_modal_checkout' + ], + 'separate_gateways' => [ 'title' => __( 'Separate Payment Gateways', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'checkbox', 'default' => 'no', 'desc' => _x( 'Make all supported and enabled payment methods available as their own payment gateway. This opens new possibilities like discounts for specific payment methods. See our full guide here', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'id' => 'btcpay_gf_separate_gateways' ], - 'customer_data' => [ + 'customer_data' => [ 'title' => __( 'Send customer data to BTCPayServer', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'checkbox', 'default' => 'no', 'desc' => _x( 'If you want customer email, address, etc. sent to BTCPay Server enable this option. By default for privacy and GDPR reasons this is disabled.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'id' => 'btcpay_gf_send_customer_data' ], - 'sats_mode' => [ + 'sats_mode' => [ 'title' => __( 'Sats-Mode', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'checkbox', 'default' => 'no', 'desc' => _x( 'Makes Satoshis/Sats available as currency "SAT" (can be found in WooCommerce->Settings->General) and handles conversion to BTC before creating the invoice on BTCPay.', 'global_settings', 'btcpay-greenfield-for-woocommerce' ), 'id' => 'btcpay_gf_sats_mode' ], - 'debug' => [ + 'debug' => [ 'title' => __( 'Debug Log', 'btcpay-greenfield-for-woocommerce' ), 'type' => 'checkbox', 'default' => 'no', diff --git a/src/Gateway/AbstractGateway.php b/src/Gateway/AbstractGateway.php index 1144050..1067fcd 100644 --- a/src/Gateway/AbstractGateway.php +++ b/src/Gateway/AbstractGateway.php @@ -8,8 +8,6 @@ use BTCPayServer\Client\InvoiceCheckoutOptions; use BTCPayServer\Client\PullPayment; use BTCPayServer\Util\PreciseNumber; -use BTCPayServer\WC\Admin\Notice; -use BTCPayServer\WC\Helper\GreenfieldApiAuthorization; use BTCPayServer\WC\Helper\GreenfieldApiHelper; use BTCPayServer\WC\Helper\GreenfieldApiWebhook; use BTCPayServer\WC\Helper\Logger; @@ -41,7 +39,8 @@ public function __construct() { $this->debug_plugin_version = BTCPAYSERVER_VERSION; // Actions. - add_action('admin_enqueue_scripts', [$this, 'addScripts']); + add_action('admin_enqueue_scripts', [$this, 'addAdminScripts']); + add_action('wp_enqueue_scripts', [$this, 'addPublicScripts']); add_action('woocommerce_update_options_payment_gateways_' . $this->getId(), [$this, 'process_admin_options']); // Supported features. @@ -102,14 +101,24 @@ public function process_payment( $orderId ) { throw new \Exception( $message ); } + // Check if the order is a modal payment. + if (isset($_POST['action'])) { + $action = wc_clean( wp_unslash( $_POST['action'] ) ); + if ( $action === 'btcpaygf_modal_checkout' ) { + Logger::debug( 'process_payment called via modal checkout.' ); + } + } + // Check for existing invoice and redirect instead. if ( $this->validInvoiceExists( $orderId ) ) { $existingInvoiceId = get_post_meta( $orderId, 'BTCPay_id', true ); Logger::debug( 'Found existing BTCPay Server invoice and redirecting to it. Invoice id: ' . $existingInvoiceId ); return [ - 'result' => 'success', + 'result' => 'success', 'redirect' => $this->apiHelper->getInvoiceRedirectUrl( $existingInvoiceId ), + 'invoiceId' => $existingInvoiceId, + 'orderCompleteLink' => $order->get_checkout_order_received_url(), ]; } @@ -128,8 +137,10 @@ public function process_payment( $orderId ) { } */ return [ - 'result' => 'success', + 'result' => 'success', 'redirect' => $url, + 'invoiceId' => $invoice->getData()['id'], + 'orderCompleteLink' => $order->get_checkout_order_received_url(), ]; } } @@ -311,7 +322,7 @@ public function getIcon(): string { /** * Add scripts. */ - public function addScripts($hook_suffix) { + public function addAdminScripts($hook_suffix) { if ($hook_suffix === 'woocommerce_page_wc-settings') { wp_enqueue_media(); wp_register_script( @@ -332,6 +343,46 @@ public function addScripts($hook_suffix) { } } + public function addPublicScripts() { + // We only load the modal checkout scripts when enabled. + if (get_option('btcpay_gf_modal_checkout') !== 'yes') { + return; + } + + if ($this->apiHelper->configured === false) { + return; + } + + // Load BTCPay modal JS. + wp_enqueue_script( 'btcpay_gf_modal_js', $this->apiHelper->url . '/modal/btcpay.js', [], BTCPAYSERVER_VERSION ); + + // Register modal script. + wp_register_script( + 'btcpay_gf_modal_checkout', + BTCPAYSERVER_PLUGIN_URL . 'assets/js/modalCheckout.js', + [ 'jquery' ], + BTCPAYSERVER_VERSION, + true + ); + + // Pass object BTCPayWP to be available on the frontend. + wp_localize_script( 'btcpay_gf_modal_checkout', 'BTCPayWP', [ + 'modalEnabled' => get_option('btcpay_gf_modal_checkout') === 'yes', + 'debugEnabled' => get_option('btcpay_gf_debug') === 'yes', + 'url' => admin_url( 'admin-ajax.php' ), + 'apiUrl' => $this->apiHelper->url, + 'apiNonce' => wp_create_nonce( 'btcpay-nonce' ), + 'isChangePaymentPage' => isset( $_GET['change_payment_method'] ) ? 'yes' : 'no', + 'isPayForOrderPage' => is_wc_endpoint_url( 'order-pay' ) ? 'yes' : 'no', + 'isAddPaymentMethodPage' => is_add_payment_method_page() ? 'yes' : 'no', + 'textInvoiceExpired' => _x('The invoice expired. Please try again, choose a different payment method or contact us if you paid but the payment did not confirm in time.', 'js', 'btcpay-greenfield-for-woocommerce'), + 'textModalClosed' => _x('Payment aborted by you. Please try again or choose a different payment method.', 'js', 'btcpay-greenfield-for-woocommerce'), + ] ); + + // Add the registered modal script to frontend. + wp_enqueue_script( 'btcpay_gf_modal_checkout' ); + } + /** * Process webhooks from BTCPay. */