Merge pull request #2372 from woocommerce/PCP-3250-apple-pay-and-google-pay-buttons-dont-appear-in-pay-pal-button-stack-on-multi-step-checkout

Apple Pay and Google Pay buttons don't appear in PayPal Button stack on multi-step Checkout (3250)
This commit is contained in:
Emili Castells 2024-07-02 11:05:09 +02:00 committed by GitHub
commit 6785ca9b06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 164 additions and 20 deletions

View file

@ -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,

View file

@ -0,0 +1,37 @@
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
const REFRESH_BUTTON_EVENT = 'ppcp_refresh_payment_buttons';
/**
* Triggers a refresh of the payment buttons.
* This function dispatches a custom event that the button components listen for.
*
* Use this function on the front-end to update payment buttons after the checkout form was updated.
*/
export function refreshButtons() {
document.dispatchEvent(new Event(REFRESH_BUTTON_EVENT));
}
/**
* Sets up event listeners for various cart and checkout update events.
* When these events occur, it triggers a refresh of the payment buttons.
*
* @param {Function} refresh - Callback responsible to re-render the payment button.
*/
export function setupButtonEvents(refresh) {
const miniCartInitDelay = 1000;
const debouncedRefresh = debounce(refresh, 50);
// Listen for our custom refresh event.
document.addEventListener(REFRESH_BUTTON_EVENT, debouncedRefresh);
// Listen for cart and checkout update events.
document.body.addEventListener('updated_cart_totals', debouncedRefresh);
document.body.addEventListener('updated_checkout', debouncedRefresh);
// Use setTimeout for fragment events to avoid unnecessary refresh on initial render.
setTimeout(() => {
document.body.addEventListener('wc_fragments_loaded', debouncedRefresh);
document.body.addEventListener('wc_fragments_refreshed', debouncedRefresh);
}, miniCartInitDelay);
}

View file

@ -0,0 +1,120 @@
import { refreshButtons } from './ButtonRefreshHelper';
const DEFAULT_TRIGGER_ELEMENT_SELECTOR = '.woocommerce-checkout-payment';
/**
* 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 {
/**
* Selector that defines the HTML element we are waiting to become visible.
* @type {string}
*/
#triggerElementSelector;
/**
* 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
* @param {string} triggerElementSelector - Optional. Selector of the dependant element.
*/
constructor(formSelector, triggerElementSelector = '') {
this.#formSelector = formSelector;
this.#triggerElementSelector = triggerElementSelector || DEFAULT_TRIGGER_ELEMENT_SELECTOR;
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) {
refreshButtons();
this.stop();
}
}
}
export default MultistepCheckoutHelper;