diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 402781bbf..fddc82a57 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -9,6 +9,15 @@ import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler"; import MessageRenderer from "./modules/Renderer/MessageRenderer"; import Spinner from "./modules/Helper/Spinner"; +import { + getCurrentPaymentMethod, + ORDER_BUTTON_SELECTOR, + PaymentMethods +} from "./modules/Helper/CheckoutMethodState"; +import {hide, setVisible} from "./modules/Helper/Hiding"; +import {isChangePaymentPage} from "./modules/Helper/Subscriptions"; + +const buttonsSpinner = new Spinner('.ppc-button-wrapper'); const bootstrap = () => { const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic); @@ -17,7 +26,10 @@ const bootstrap = () => { const onSmartButtonClick = data => { window.ppcpFundingSource = data.fundingSource; }; - const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick); + const onSmartButtonsInit = () => { + buttonsSpinner.unblock(); + }; + const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const context = PayPalCommerceGateway.context; if (context === 'mini-cart' || context === 'product') { @@ -91,8 +103,48 @@ document.addEventListener( return; } + // Sometimes PayPal script takes long time to load, + // so we additionally hide the standard order button here to avoid failed orders. + // Normally it is hidden later after the script load. + const hideOrderButtonIfPpcpGateway = () => { + // only in checkout and pay now page, otherwise it may break things (e.g. payment via product page), + // and also the loading spinner may look weird on other pages + if ( + !['checkout', 'pay-now'].includes(PayPalCommerceGateway.context) + || isChangePaymentPage() + ) { + return; + } + + const currentPaymentMethod = getCurrentPaymentMethod(); + const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; + + setVisible(ORDER_BUTTON_SELECTOR, !isPaypal, true); + + if (isPaypal) { + // stopped after the first rendering of the buttons, in onInit + buttonsSpinner.block(); + } else { + buttonsSpinner.unblock(); + } + } + + let bootstrapped = false; + + hideOrderButtonIfPpcpGateway(); + + jQuery(document.body).on('updated_checkout payment_method_selected', () => { + if (bootstrapped) { + return; + } + + hideOrderButtonIfPpcpGateway(); + }); + const script = document.createElement('script'); script.addEventListener('load', (event) => { + bootstrapped = true; + bootstrap(); }); script.setAttribute('src', PayPalCommerceGateway.button.url); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index 9519f9073..5ed0d60ff 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -1,6 +1,11 @@ import ErrorHandler from '../ErrorHandler'; import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; import { setVisible } from '../Helper/Hiding'; +import { + getCurrentPaymentMethod, + isSavedCardSelected, ORDER_BUTTON_SELECTOR, + PaymentMethods +} from "../Helper/CheckoutMethodState"; class CheckoutBootstap { constructor(gateway, renderer, messages, spinner) { @@ -9,7 +14,7 @@ class CheckoutBootstap { this.messages = messages; this.spinner = spinner; - this.standardOrderButtonSelector = '#place_order'; + this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR; this.buttonChangeObserver = new MutationObserver((el) => { this.updateUi(); @@ -76,10 +81,10 @@ class CheckoutBootstap { } updateUi() { - const currentPaymentMethod = this.currentPaymentMethod(); - const isPaypal = currentPaymentMethod === 'ppcp-gateway'; - const isCard = currentPaymentMethod === 'ppcp-credit-card-gateway'; - const isSavedCard = isCard && this.isSavedCardSelected(); + const currentPaymentMethod = getCurrentPaymentMethod(); + const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL; + const isCard = currentPaymentMethod === PaymentMethods.CARDS; + const isSavedCard = isCard && isSavedCardSelected(); const isNotOurGateway = !isPaypal && !isCard; setVisible(this.standardOrderButtonSelector, isNotOurGateway || isSavedCard, true); @@ -125,15 +130,6 @@ class CheckoutBootstap { jQuery('#ppcp-credit-card-vault').attr("disabled", false) this.renderer.enableCreditCardFields() } - - currentPaymentMethod() { - return jQuery('input[name="payment_method"]:checked').val(); - } - - isSavedCardSelected() { - const savedCardList = jQuery('#saved-credit-card'); - return savedCardList.length && savedCardList.val() !== ''; - } } export default CheckoutBootstap diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js index cd52d4b9e..4b2583c98 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/PayNowBootstrap.js @@ -1,4 +1,5 @@ import CheckoutBootstap from './CheckoutBootstap' +import {isChangePaymentPage} from "../Helper/Subscriptions"; class PayNowBootstrap extends CheckoutBootstap { constructor(gateway, renderer, messages, spinner) { @@ -6,8 +7,7 @@ class PayNowBootstrap extends CheckoutBootstap { } updateUi() { - const urlParams = new URLSearchParams(window.location.search) - if (urlParams.has('change_payment_method')) { + if (isChangePaymentPage()) { return } diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js new file mode 100644 index 000000000..196ad596b --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js @@ -0,0 +1,20 @@ +export const PaymentMethods = { + PAYPAL: 'ppcp-gateway', + CARDS: 'ppcp-credit-card-gateway', +}; + +export const ORDER_BUTTON_SELECTOR = '#place_order'; + +export const getCurrentPaymentMethod = () => { + const el = document.querySelector('input[name="payment_method"]:checked'); + if (!el) { + return null; + } + + return el.value; +}; + +export const isSavedCardSelected = () => { + const savedCardList = document.querySelector('#saved-credit-card'); + return savedCardList && savedCardList.value !== ''; +}; diff --git a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js index 9c41eb08b..673e37a90 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/Spinner.js +++ b/modules/ppcp-button/resources/js/modules/Helper/Spinner.js @@ -1,7 +1,7 @@ class Spinner { - constructor() { - this.target = 'form.woocommerce-checkout'; + constructor(target = 'form.woocommerce-checkout') { + this.target = target; } setTarget(target) { diff --git a/modules/ppcp-button/resources/js/modules/Helper/Subscriptions.js b/modules/ppcp-button/resources/js/modules/Helper/Subscriptions.js new file mode 100644 index 000000000..a205ac459 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/Subscriptions.js @@ -0,0 +1,4 @@ +export const isChangePaymentPage = () => { + const urlParams = new URLSearchParams(window.location.search) + return urlParams.has('change_payment_method'); +} diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index 43660c30c..cc811bb58 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -1,8 +1,9 @@ class Renderer { - constructor(creditCardRenderer, defaultConfig, onSmartButtonClick) { + constructor(creditCardRenderer, defaultConfig, onSmartButtonClick, onSmartButtonsInit) { this.defaultConfig = defaultConfig; this.creditCardRenderer = creditCardRenderer; this.onSmartButtonClick = onSmartButtonClick; + this.onSmartButtonsInit = onSmartButtonsInit; } render(wrapper, hostedFieldsWrapper, contextConfig) { @@ -21,6 +22,7 @@ class Renderer { style, ...contextConfig, onClick: this.onSmartButtonClick, + onInit: this.onSmartButtonsInit, }).render(wrapper); } diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 7e6bb7ad1..896a357c4 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -458,7 +458,9 @@ class SmartButton implements SmartButtonInterface { return; } - echo '
'; + // The wrapper is needed for the loading spinner, + // otherwise jQuery block() prevents buttons rendering. + echo '
'; } /**