diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index d0e1a8543..e680115ba 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -17,6 +17,7 @@ import { import {setVisibleByClass} from "./modules/Helper/Hiding"; import {isChangePaymentPage} from "./modules/Helper/Subscriptions"; import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler"; +import MultistepCheckoutHelper from "./modules/Helper/MultistepCheckoutHelper"; import FormSaver from './modules/Helper/FormSaver'; import FormValidator from "./modules/Helper/FormValidator"; import {loadPaypalScript} from "./modules/Helper/ScriptLoading"; @@ -53,6 +54,8 @@ const bootstrap = () => { const freeTrialHandler = new FreeTrialHandler(PayPalCommerceGateway, checkoutFormSelector, formSaver, formValidator, spinner, errorHandler); + new MultistepCheckoutHelper(checkoutFormSelector); + jQuery('form.woocommerce-checkout input').on('keydown', e => { if (e.key === 'Enter' && [ PaymentMethods.PAYPAL, diff --git a/modules/ppcp-button/resources/js/modules/Helper/MultistepCheckoutHelper.js b/modules/ppcp-button/resources/js/modules/Helper/MultistepCheckoutHelper.js new file mode 100644 index 000000000..c4eec6143 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/MultistepCheckoutHelper.js @@ -0,0 +1,121 @@ +/** + * The MultistepCheckoutHelper class ensures the initialization of payment buttons + * on websites using a multistep checkout plugin. These plugins usually hide the + * payment button on page load up and reveal it later using JS. During the + * invisibility period of wrappers, some payment buttons fail to initialize, + * so we wait for the payment element to be visible. + * + * @property {HTMLElement} form - Checkout form element. + * @property {HTMLElement} triggerElement - Element, which visibility we need to detect. + * @property {boolean} isVisible - Whether the triggerElement is visible. + */ +class MultistepCheckoutHelper { + + /** + * Configuration that defines the HTML selector for the component we are waiting to be visible. + * @type {string} + */ + #triggerElementSelector = '.woocommerce-checkout-payment'; + + /** + * Interval (in milliseconds) in which the visibility of the trigger element is checked. + * @type {number} + */ + #intervalTime = 150; + + /** + * The interval ID returned by the setInterval() method. + * @type {number|false} + */ + #intervalId; + + /** + * Selector passed to the constructor that identifies the checkout form + * @type {string} + */ + #formSelector; + + /** + * @param {string} formSelector - Selector of the checkout form + */ + constructor(formSelector) { + this.#formSelector = formSelector; + this.#intervalId = false; + + /* + Start the visibility checker after a brief delay. This allows eventual multistep plugins to + dynamically prepare the checkout page, so we can decide whether this helper is needed. + */ + setTimeout(() => { + if (this.form && !this.isVisible) { + this.start(); + } + }, 250); + } + + /** + * The checkout form element. + * @returns {Element|null} - Form element or null. + */ + get form() { + return document.querySelector(this.#formSelector); + } + + /** + * The element which must be visible before payment buttons should be initialized. + * @returns {Element|null} - Trigger element or null. + */ + get triggerElement() { + return this.form?.querySelector(this.#triggerElementSelector); + } + + /** + * Checks the visibility of the payment button wrapper. + * @returns {boolean} - returns boolean value on the basis of visibility of element. + */ + get isVisible() { + const box = this.triggerElement?.getBoundingClientRect(); + + return !!(box && box.width && box.height); + } + + /** + * Starts the observation of the DOM, initiates monitoring the checkout form. + * To ensure multiple calls to start don't create multiple intervals, we first call stop. + */ + start() { + this.stop(); + this.#intervalId = setInterval(() => this.checkElement(), this.#intervalTime); + } + + /** + * Stops the observation of the checkout form. + * Multiple calls to stop are safe as clearInterval doesn't throw if provided ID doesn't exist. + */ + stop() { + if (this.#intervalId) { + clearInterval(this.#intervalId); + this.#intervalId = false; + } + } + + /** + * Checks if the trigger element is visible. + * If visible, it initialises the payment buttons and stops the observation. + */ + checkElement() { + if (this.isVisible) { + this.refreshButtons(); + this.stop(); + } + } + + /** + * Initializes the payment buttons on the visibility of wrapper. + */ + refreshButtons() { + document.dispatchEvent(new Event('ppcp_refresh_payment_buttons')); + } +} + +export default MultistepCheckoutHelper;