From 95c7d4f7bcaf4d46639aa822e89ec3589b21bff2 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 7 Aug 2024 18:26:02 +0200
Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20PaymentButton?=
=?UTF-8?q?=20creation=20code?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../js/modules/Renderer/PaymentButton.js | 278 +++++++++++++-----
.../resources/js/GooglepayButton.js | 67 +++--
2 files changed, 237 insertions(+), 108 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
index 9e1def508..6013b2273 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -26,22 +26,53 @@ import {
* @property {string} SmartButton - Wrapper for smart button container.
*/
+/**
+ * Adds the provided PaymentButton instance to a global payment-button collection.
+ *
+ * This is debugging logic that should not be used on a production site.
+ *
+ * @param {string} methodName - Used to group the buttons.
+ * @param {PaymentButton} button - Appended to the button collection.
+ */
+const addToDebuggingCollection = ( methodName, button ) => {
+ window.ppcpPaymentButtonList = window.ppcpPaymentButtonList || {};
+
+ const collection = window.ppcpPaymentButtonList;
+
+ collection[ methodName ] = collection[ methodName ] || [];
+ collection[ methodName ].push( button );
+};
+
/**
* Base class for APM payment buttons, like GooglePay and ApplePay.
*
* This class is not intended for the PayPal button.
*/
export default class PaymentButton {
+ /**
+ * Defines the implemented payment method.
+ *
+ * Used to identify and address the button internally.
+ * Overwrite this in the derived class.
+ *
+ * @type {string}
+ */
+ static methodId = 'generic';
+
+ /**
+ * CSS class that is added to the payment button wrapper.
+ *
+ * Overwrite this in the derived class.
+ *
+ * @type {string}
+ */
+ static cssClass = '';
+
/**
* @type {ConsoleLogger}
*/
#logger;
- /**
- * @type {string}
- */
- #methodId;
-
/**
* Whether the payment button is initialized.
*
@@ -70,7 +101,7 @@ export default class PaymentButton {
#styles;
/**
- * APM relevant configuration; e.g., configuration of the GooglePay button
+ * APM relevant configuration; e.g., configuration of the GooglePay button.
*/
#buttonConfig;
@@ -101,38 +132,72 @@ export default class PaymentButton {
*/
#button = null;
+ /**
+ * Returns a list with all wrapper IDs for the implemented payment method, categorized by context.
+ *
+ * @abstract
+ * @param {Object} buttonConfig - Payment method specific configuration.
+ * @param {Object} ppcpConfig - Global plugin configuration.
+ * @return {{MiniCart, Gateway, Block, SmartButton, Default}} The wrapper ID collection.
+ */
+ // eslint-disable-next-line no-unused-vars
+ static getWrappers( buttonConfig, ppcpConfig ) {
+ throw new Error( 'Must be implemented in the child class' );
+ }
+
+ /**
+ * Returns a list of all button styles for the implemented payment method, categorized by context.
+ *
+ * @abstract
+ * @param {Object} buttonConfig - Payment method specific configuration.
+ * @param {Object} ppcpConfig - Global plugin configuration.
+ * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
+ */
+ // eslint-disable-next-line no-unused-vars
+ static getStyles( buttonConfig, ppcpConfig ) {
+ throw new Error( 'Must be implemented in the child class' );
+ }
+
/**
* Initialize the payment button instance.
*
- * @param {string} methodId - Payment method ID (slug, e.g., "ppcp-googlepay").
- * @param {string} context - Button context name.
- * @param {WrapperCollection} wrappers - Button wrapper IDs, by context.
- * @param {StylesCollection} styles - Button styles, by context.
- * @param {Object} buttonConfig - Payment button specific configuration.
- * @param {Object} ppcpConfig - Plugin wide configuration object.
+ * Do not create new button instances directly; use the `createButton` method instead
+ * to avoid multiple button instances handling the same context.
+ *
+ * @private
+ * @param {string} context - Button context name.
+ * @param {Object} buttonConfig - Payment button specific configuration.
+ * @param {Object} ppcpConfig - Plugin wide configuration object.
*/
- constructor(
- methodId,
- context,
- wrappers,
- styles,
- buttonConfig,
- ppcpConfig
- ) {
- const methodName = methodId.replace( /^ppcp?-/, '' );
+ constructor( context, buttonConfig, ppcpConfig ) {
+ if ( this.methodId === PaymentButton.methodId ) {
+ throw new Error( 'Cannot initialize the PaymentButton base class' );
+ }
- this.#methodId = methodId;
-
- this.#logger = new ConsoleLogger( methodName, context );
- this.#logger.enabled = !! buttonConfig?.is_debug;
+ const isDebugging = !! buttonConfig?.is_debug;
+ const methodName = this.methodId.replace( /^ppcp?-/, '' );
this.#context = context;
- this.#wrappers = wrappers;
- this.#styles = styles;
this.#buttonConfig = buttonConfig;
this.#ppcpConfig = ppcpConfig;
- apmButtonsInit( ppcpConfig );
+ this.#wrappers = this.constructor.getWrappers(
+ this.#buttonConfig,
+ this.#ppcpConfig
+ );
+ this.#styles = this.constructor.getStyles(
+ this.#buttonConfig,
+ this.#ppcpConfig
+ );
+
+ this.#logger = new ConsoleLogger( methodName, context );
+
+ if ( isDebugging ) {
+ this.#logger.enabled = true;
+ addToDebuggingCollection( methodName, this );
+ }
+
+ apmButtonsInit( this.#ppcpConfig );
this.initEventListeners();
}
@@ -140,10 +205,20 @@ export default class PaymentButton {
* Internal ID of the payment gateway.
*
* @readonly
- * @return {string} The internal gateway ID.
+ * @return {string} The internal gateway ID, defined in the derived class.
*/
get methodId() {
- return this.#methodId;
+ return this.constructor.methodId;
+ }
+
+ /**
+ * CSS class that is added to the button wrapper.
+ *
+ * @readonly
+ * @return {string} CSS class, defined in the derived class.
+ */
+ get cssClass() {
+ return this.constructor.cssClass;
}
/**
@@ -168,6 +243,24 @@ export default class PaymentButton {
return this.#context;
}
+ /**
+ * Configuration, specific for the implemented payment button.
+ *
+ * @return {Object} Configuration object.
+ */
+ get buttonConfig() {
+ return this.#buttonConfig;
+ }
+
+ /**
+ * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
+ *
+ * @return {Object} Configuration object.
+ */
+ get ppcpConfig() {
+ return this.#ppcpConfig;
+ }
+
/**
* Button wrapper details.
*
@@ -380,6 +473,10 @@ export default class PaymentButton {
}
triggerRedraw() {
+ if ( this.isEligible && this.isSeparateGateway ) {
+ this.showPaymentGateway();
+ }
+
dispatchButtonEvent( {
event: ButtonEvents.REDRAW,
paymentMethod: this.methodId,
@@ -418,46 +515,75 @@ export default class PaymentButton {
* Refreshes the payment button on the page.
*/
refresh() {
- const showButtonWrapper = () => {
- this.log( 'Show' );
-
- const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`;
-
- document
- .querySelectorAll( styleSelectors )
- .forEach( ( el ) => el.remove() );
-
- this.allElements.forEach( ( element ) => {
- element.style.display = 'block';
- } );
- };
-
- const hideButtonWrapper = () => {
- this.log( 'Hide' );
-
- this.allElements.forEach( ( element ) => {
- element.style.display = 'none';
- } );
- };
-
- // Refresh or hide the actual payment button.
- if ( this.isVisible ) {
- this.addButton();
- } else {
- this.removeButton();
+ if ( ! this.isPresent ) {
+ return;
}
- // Show the wrapper or gateway entry, i.e. add space for the button.
- if ( this.isEligible && this.isPresent ) {
- showButtonWrapper();
+ this.applyWrapperStyles();
+
+ // Refresh or hide the actual payment button.
+ if ( this.isEligible && this.isPresent && this.isVisible ) {
+ this.addButton();
} else {
- hideButtonWrapper();
+ // this.removeButton();
}
}
+ /**
+ * Makes the custom payment gateway visible by removing initial inline styles from the DOM.
+ *
+ * Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
+ */
+ showPaymentGateway() {
+ const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`;
+
+ const styles = document.querySelectorAll( styleSelectors );
+
+ if ( ! styles.length ) {
+ return;
+ }
+ this.log( 'Show gateway' );
+
+ styles.forEach( ( el ) => el.remove() );
+ }
+
+ /**
+ * Applies CSS classes and inline styling to the payment button wrapper.
+ */
+ applyWrapperStyles() {
+ const wrapper = this.wrapperElement;
+ const { shape, height } = this.style;
+
+ wrapper.classList.add(
+ `ppcp-button-${ shape }`,
+ 'ppcp-button-apm',
+ this.cssClass
+ );
+
+ if ( height ) {
+ wrapper.style.height = `${ height }px`;
+ }
+
+ // Apply the wrapper visibility.
+ wrapper.style.display = this.isVisible ? 'block' : 'none';
+ }
+
+ /**
+ * Creates a new payment button (HTMLElement) and must call `this.insertButton()` to display
+ * that button in the correct wrapper.
+ *
+ * @abstract
+ */
+ addButton() {
+ throw new Error( 'Must be implemented by the child class' );
+ }
+
/**
* Prepares the button wrapper element and inserts the provided payment button into the DOM.
*
+ * If a payment button was previously inserted to the wrapper, calling this method again will
+ * first remove the previous button.
+ *
* @param {HTMLElement} button - The button element to inject.
*/
insertButton( button ) {
@@ -465,28 +591,16 @@ export default class PaymentButton {
return;
}
+ this.log( 'addButton', button );
+ const wrapper = this.wrapperElement;
+
if ( this.#button ) {
- this.#button.remove();
+ this.log( 'addButton.removePrevious', this.#button );
+ wrapper.removeChild( this.#button );
}
this.#button = button;
- this.log( 'addButton', button );
-
- const wrapper = this.wrapperElement;
- const { shape, height } = this.style;
- const methodSlug = this.methodId.replace( /^ppcp?-/, '' );
-
- wrapper.classList.add(
- `ppcp-button-${ shape }`,
- 'ppcp-button-apm',
- `ppcp-button-${ methodSlug }`
- );
-
- if ( height ) {
- wrapper.style.height = `${ height }px`;
- }
-
- wrapper.appendChild( button );
+ wrapper.appendChild( this.#button );
}
/**
@@ -500,8 +614,10 @@ export default class PaymentButton {
this.log( 'removeButton' );
if ( this.#button ) {
- this.#button.remove();
+ const wrapper = this.wrapperElement;
+ wrapper.removeChild( this.#button );
+
+ this.#button = null;
}
- this.#button = null;
}
}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index ae9120b52..cf2d7f067 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,5 +1,3 @@
-/* global google */
-
import {
combineStyles,
combineWrapperIds,
@@ -49,11 +47,47 @@ import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper
*/
class GooglepayButton extends PaymentButton {
+ /**
+ * @inheritDoc
+ */
+ static methodId = PaymentMethods.GOOGLEPAY;
+
+ /**
+ * @inheritDoc
+ */
+ static cssClass = 'google-pay';
+
/**
* Client reference, provided by the Google Pay JS SDK.
*/
#paymentsClient = null;
+ /**
+ * @inheritDoc
+ */
+ static getWrappers( buttonConfig, ppcpConfig ) {
+ return combineWrapperIds(
+ buttonConfig.button.wrapper,
+ buttonConfig.button.mini_cart_wrapper,
+ ppcpConfig.button.wrapper,
+ 'express-payment-method-ppcp-googlepay',
+ 'ppc-button-ppcp-googlepay'
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ static getStyles( buttonConfig, ppcpConfig ) {
+ const styles = combineStyles( ppcpConfig.button, buttonConfig.button );
+
+ if ( 'buy' === styles.MiniCart.type ) {
+ styles.MiniCart.type = 'pay';
+ }
+
+ return styles;
+ }
+
constructor(
context,
externalHandler,
@@ -61,30 +95,8 @@ class GooglepayButton extends PaymentButton {
ppcpConfig,
contextHandler
) {
- const wrappers = combineWrapperIds(
- buttonConfig.button.wrapper,
- buttonConfig.button.mini_cart_wrapper,
- ppcpConfig.button.wrapper,
- 'express-payment-method-ppcp-googlepay',
- 'ppc-button-ppcp-googlepay'
- );
+ super( context, buttonConfig, ppcpConfig );
- const styles = combineStyles( ppcpConfig.button, buttonConfig.button );
-
- if ( 'buy' === styles.MiniCart.type ) {
- styles.MiniCart.type = 'pay';
- }
-
- super(
- PaymentMethods.GOOGLEPAY,
- context,
- wrappers,
- styles,
- buttonConfig,
- ppcpConfig
- );
-
- this.buttonConfig = buttonConfig;
this.contextHandler = contextHandler;
this.log( 'Create instance' );
@@ -226,10 +238,11 @@ class GooglepayButton extends PaymentButton {
}
/**
- * Add a Google Pay purchase button.
+ * Creates the payment button and calls `this.insertButton()` to make the button visible in the
+ * correct wrapper.
*/
addButton() {
- if ( ! this.isInitialized ) {
+ if ( ! this.isInitialized || ! this.paymentsClient ) {
return;
}