From 8afa7e34dc9342c99817afa5e08fe68329053521 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Mon, 29 Jul 2024 16:20:47 +0200
Subject: [PATCH 01/40] =?UTF-8?q?=F0=9F=90=9B=20Prevent=20duplicate=20paym?=
=?UTF-8?q?ent=20button=20instances?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/GooglepayButton.js | 37 +++++++++++++++++--
1 file changed, 33 insertions(+), 4 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 87bc642f0..723f9dd8c 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -5,6 +5,19 @@ import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
class GooglepayButton {
+ /**
+ * Reference to the payment button created by this instance.
+ *
+ * @type {HTMLElement}
+ */
+ #button;
+
+ /**
+ * Client reference, provided by the Google Pay JS SDK.
+ * @see https://developers.google.com/pay/api/web/reference/client
+ */
+ paymentsClient = null;
+
constructor(
context,
externalHandler,
@@ -22,8 +35,6 @@ class GooglepayButton {
this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
- this.paymentsClient = null;
-
this.log = function () {
if ( this.buttonConfig.is_debug ) {
//console.log('[GooglePayButton]', ...arguments);
@@ -235,13 +246,19 @@ class GooglepayButton {
const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
this.waitForWrapper( wrapper, () => {
+ // Prevent duplicate payment buttons.
+ this.removeButton();
+
jQuery( wrapper ).addClass( 'ppcp-button-' + ppcpStyle.shape );
if ( ppcpStyle.height ) {
jQuery( wrapper ).css( 'height', `${ ppcpStyle.height }px` );
}
- const button = this.paymentsClient.createButton( {
+ /**
+ * @see https://developers.google.com/pay/api/web/reference/client#createButton
+ */
+ this.#button = this.paymentsClient.createButton( {
onClick: this.onButtonClick.bind( this ),
allowedPaymentMethods: [ baseCardPaymentMethod ],
buttonColor: buttonStyle.color || 'black',
@@ -250,10 +267,22 @@ class GooglepayButton {
buttonSizeMode: 'fill',
} );
- jQuery( wrapper ).append( button );
+ jQuery( wrapper ).append( this.#button );
} );
}
+ /**
+ * Removes the payment button that was injected via addButton()
+ */
+ removeButton() {
+ if ( ! this.#button ) {
+ return;
+ }
+
+ this.#button.remove();
+ this.#button = null;
+ }
+
addButtonCheckout( baseCardPaymentMethod, wrapper, buttonStyle ) {
const button = this.paymentsClient.createButton( {
onClick: this.onButtonClick.bind( this ),
From 1e5b6d5a210252b6f0f49d4d7c52e6ba0ff29337 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Mon, 29 Jul 2024 18:05:08 +0200
Subject: [PATCH 02/40] =?UTF-8?q?=E2=9C=A8=20Improve=20debug-logging=20for?=
=?UTF-8?q?=20GooglePayButton=20events?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implement same logic as we use for the ApplePayButton
---
.../resources/js/GooglepayButton.js | 33 ++++++++++++++++---
1 file changed, 29 insertions(+), 4 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 723f9dd8c..aa2c27921 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -25,6 +25,8 @@ class GooglepayButton {
ppcpConfig,
contextHandler
) {
+ this._initDebug( !! buttonConfig?.is_debug, context );
+
apmButtonsInit( ppcpConfig );
this.isInitialized = false;
@@ -34,12 +36,35 @@ class GooglepayButton {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
+ }
- this.log = function () {
- if ( this.buttonConfig.is_debug ) {
- //console.log('[GooglePayButton]', ...arguments);
- }
+ /**
+ * NOOP log function to avoid errors when debugging is disabled.
+ */
+ log() {}
+
+ /**
+ * Enables debugging tools, when the button's is_debug flag is set.
+ *
+ * @param {boolean} enableDebugging If debugging features should be enabled for this instance.
+ * @param {string} context Used to make the instance accessible via the global debug object.
+ * @private
+ */
+ _initDebug( enableDebugging, context ) {
+ if ( ! enableDebugging ) {
+ return;
+ }
+
+ document.ppcpGooglepayButtons = document.ppcpGooglepayButtons || {};
+ document.ppcpGooglepayButtons[ context ] = this;
+
+ this.log = ( ...args ) => {
+ console.log( `[GooglePayButton | ${ context }]`, ...args );
};
+
+ document.addEventListener( 'ppcp-googlepay-debug', () => {
+ this.log( this );
+ } );
}
init( config, transactionInfo ) {
From 8814b1f6368d997a5191269ccaad171e819c094e Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Mon, 29 Jul 2024 18:30:03 +0200
Subject: [PATCH 03/40] =?UTF-8?q?=F0=9F=92=A1=20Improve/adjust=20debug=20l?=
=?UTF-8?q?ogging?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/GooglepayButton.js | 40 ++++++++++---------
1 file changed, 22 insertions(+), 18 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index aa2c27921..9ee34fa0f 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -36,6 +36,8 @@ class GooglepayButton {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
+
+ this.log( 'Create instance' );
}
/**
@@ -47,7 +49,8 @@ class GooglepayButton {
* Enables debugging tools, when the button's is_debug flag is set.
*
* @param {boolean} enableDebugging If debugging features should be enabled for this instance.
- * @param {string} context Used to make the instance accessible via the global debug object.
+ * @param {string} context Used to make the instance accessible via the global debug
+ * object.
* @private
*/
_initDebug( enableDebugging, context ) {
@@ -81,6 +84,8 @@ class GooglepayButton {
return;
}
+ this.log( 'Init' );
+
this.googlePayConfig = config;
this.transactionInfo = transactionInfo;
this.allowedPaymentMethods = config.allowedPaymentMethods;
@@ -218,9 +223,13 @@ class GooglepayButton {
this.onPaymentDataChanged.bind( this );
}
+ /**
+ * Consider providing merchant info here:
+ *
+ * @see https://developers.google.com/pay/api/web/reference/request-objects#PaymentOptions
+ */
this.paymentsClient = new google.payments.api.PaymentsClient( {
environment: this.buttonConfig.environment,
- // add merchant info maybe
paymentDataCallbacks: callbacks,
} );
}
@@ -266,7 +275,7 @@ class GooglepayButton {
* @param baseCardPaymentMethod
*/
addButton( baseCardPaymentMethod ) {
- this.log( 'addButton', this.context );
+ this.log( 'addButton' );
const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
@@ -344,17 +353,14 @@ class GooglepayButton {
* Show Google Pay payment sheet when Google Pay payment button is clicked
*/
onButtonClick() {
- this.log( 'onButtonClick', this.context );
+ this.log( 'onButtonClick' );
const paymentDataRequest = this.paymentDataRequest();
- this.log(
- 'onButtonClick: paymentDataRequest',
- paymentDataRequest,
- this.context
- );
+ this.log( 'onButtonClick: paymentDataRequest', paymentDataRequest );
- window.ppcpFundingSource = 'googlepay'; // Do this on another place like on create order endpoint handler.
+ // Do this on another place like on create order endpoint handler.
+ window.ppcpFundingSource = 'googlepay';
this.paymentsClient.loadPaymentData( paymentDataRequest );
}
@@ -404,8 +410,7 @@ class GooglepayButton {
}
onPaymentDataChanged( paymentData ) {
- this.log( 'onPaymentDataChanged', this.context );
- this.log( 'paymentData', paymentData );
+ this.log( 'onPaymentDataChanged', paymentData );
return new Promise( async ( resolve, reject ) => {
try {
@@ -478,18 +483,18 @@ class GooglepayButton {
//------------------------
onPaymentAuthorized( paymentData ) {
- this.log( 'onPaymentAuthorized', this.context );
+ this.log( 'onPaymentAuthorized' );
return this.processPayment( paymentData );
}
async processPayment( paymentData ) {
- this.log( 'processPayment', this.context );
+ this.log( 'processPayment' );
return new Promise( async ( resolve, reject ) => {
try {
const id = await this.contextHandler.createOrder();
- this.log( 'processPayment: createOrder', id, this.context );
+ this.log( 'processPayment: createOrder', id );
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
@@ -500,8 +505,7 @@ class GooglepayButton {
this.log(
'processPayment: confirmOrder',
- confirmOrderResponse,
- this.context
+ confirmOrderResponse
);
/** Capture the Order on the Server */
@@ -571,7 +575,7 @@ class GooglepayButton {
};
}
- this.log( 'processPaymentResponse', response, this.context );
+ this.log( 'processPaymentResponse', response );
return response;
}
From 8286085f594372e4aba5b02aa4e566ddd01ce734 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Mon, 29 Jul 2024 21:16:53 +0200
Subject: [PATCH 04/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Apply=20latest=20JS?=
=?UTF-8?q?=20structure=20from=20ApplePay=20Gateway?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Partially addresses the known display bug
- Simplifies maintainance between both gateways
- Reduces component-internal redundancies
---
.../resources/js/GooglepayButton.js | 405 ++++++++++++------
modules/ppcp-googlepay/src/Assets/Button.php | 17 +
2 files changed, 299 insertions(+), 123 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 9ee34fa0f..9571e9735 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,16 +1,61 @@
+/* global google */
+/* global jQuery */
+
import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
+/**
+ * Plugin-specific styling.
+ *
+ * Note that most properties of this object do not apply to the Google Pay button.
+ *
+ * @typedef {Object} PPCPStyle
+ * @property {string} shape - Outline shape.
+ * @property {?number} height - Button height in pixel.
+ */
+
+/**
+ * Style options that are defined by the Google Pay SDK and are required to render the button.
+ *
+ * @typedef {Object} GooglePayStyle
+ * @property {string} type - Defines the button label.
+ * @property {string} color - Button color
+ * @property {string} language - The locale; an empty string will apply the user-agent's language.
+ */
+
+/**
+ * List of valid context values that the button can have.
+ *
+ * @type {Object}
+ */
+const CONTEXT = {
+ Product: 'product',
+ Cart: 'cart',
+ Checkout: 'checkout',
+ PayNow: 'pay-now',
+ MiniCart: 'mini-cart',
+ BlockCart: 'cart-block',
+ BlockCheckout: 'checkout-block',
+ Preview: 'preview',
+ // Block editor contexts.
+ Blocks: [ 'cart-block', 'checkout-block' ],
+ // Custom gateway contexts.
+ Gateways: [ 'checkout', 'pay-now' ],
+};
+
class GooglepayButton {
+ #wrapperId = '';
+ #ppcpButtonWrapperId = '';
+
/**
- * Reference to the payment button created by this instance.
+ * Whether the payment button is initialized.
*
- * @type {HTMLElement}
+ * @type {boolean}
*/
- #button;
+ #isInitialized = false;
/**
* Client reference, provided by the Google Pay JS SDK.
@@ -29,8 +74,6 @@ class GooglepayButton {
apmButtonsInit( ppcpConfig );
- this.isInitialized = false;
-
this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
@@ -54,7 +97,7 @@ class GooglepayButton {
* @private
*/
_initDebug( enableDebugging, context ) {
- if ( ! enableDebugging ) {
+ if ( ! enableDebugging || this.#isInitialized ) {
return;
}
@@ -70,11 +113,164 @@ class GooglepayButton {
} );
}
+ /**
+ * Determines if the current payment button should be rendered as a stand-alone gateway.
+ * The return value `false` usually means, that the payment button is bundled with all available
+ * payment buttons.
+ *
+ * The decision depends on the button context (placement) and the plugin settings.
+ *
+ * @return {boolean} True, if the current button represents a stand-alone gateway.
+ */
+ get isSeparateGateway() {
+ return (
+ this.buttonConfig.is_wc_gateway_enabled &&
+ CONTEXT.Gateways.includes( this.context )
+ );
+ }
+
+ /**
+ * Returns the wrapper ID for the current button context.
+ * The ID varies for the MiniCart context.
+ *
+ * @return {string} The wrapper-element's ID (without the `#` prefix).
+ */
+ get wrapperId() {
+ if ( ! this.#wrapperId ) {
+ let id;
+
+ if ( CONTEXT.MiniCart === this.context ) {
+ id = this.buttonConfig.button.mini_cart_wrapper;
+ } else if ( this.isSeparateGateway ) {
+ id = 'ppc-button-ppcp-googlepay';
+ } else {
+ id = this.buttonConfig.button.wrapper;
+ }
+
+ this.#wrapperId = id.replace( /^#/, '' );
+ }
+
+ return this.#wrapperId;
+ }
+
+ /**
+ * Returns the wrapper ID for the ppcpButton
+ *
+ * @return {string} The wrapper-element's ID (without the `#` prefix).
+ */
+ get ppcpButtonWrapperId() {
+ if ( ! this.#ppcpButtonWrapperId ) {
+ let id;
+
+ if ( CONTEXT.MiniCart === this.context ) {
+ id = this.ppcpConfig.button.mini_cart_wrapper;
+ } else if ( CONTEXT.Blocks.includes( this.context ) ) {
+ id = 'express-payment-method-ppcp-gateway-paypal';
+ } else {
+ id = this.ppcpConfig.button.wrapper;
+ }
+
+ this.#ppcpButtonWrapperId = id.replace( /^#/, '' );
+ }
+
+ return this.#ppcpButtonWrapperId;
+ }
+
+ /**
+ * Returns the context-relevant PPCP style object.
+ * The style for the MiniCart context can be different.
+ *
+ * The PPCP style are custom style options, that are provided by this plugin.
+ *
+ * @return {PPCPStyle} The style object.
+ */
+ get ppcpStyle() {
+ if ( CONTEXT.MiniCart === this.context ) {
+ return this.ppcpConfig.button.mini_cart_style;
+ }
+
+ return this.ppcpConfig.button.style;
+ }
+
+ /**
+ * Returns default style options that are propagated to and rendered by the Google Pay button.
+ *
+ * These styles are the official style options provided by the Google Pay SDK.
+ *
+ * @return {GooglePayStyle} The style object.
+ */
+ get buttonStyle() {
+ let style;
+
+ if ( CONTEXT.MiniCart === this.context ) {
+ style = this.buttonConfig.button.mini_cart_style;
+
+ // Handle incompatible types.
+ if ( style.type === 'buy' ) {
+ style.type = 'pay';
+ }
+ } else {
+ style = this.buttonConfig.button.style;
+ }
+
+ return {
+ type: style.type,
+ language: style.language,
+ color: style.color,
+ };
+ }
+
+ /**
+ * Returns the HTML element that wraps the current button
+ *
+ * @return {HTMLElement|null} The wrapper element, or null.
+ */
+ get wrapperElement() {
+ return document.getElementById( this.wrapperId );
+ }
+
+ /**
+ * Returns an array of HTMLElements that belong to the payment button.
+ *
+ * @return {HTMLElement[]} List of payment button wrapper elements.
+ */
+ get allElements() {
+ const selectors = [];
+
+ // Payment button (Pay now, smart button block)
+ selectors.push( `#${ this.wrapperId }` );
+
+ // Block Checkout: Express checkout button.
+ if ( CONTEXT.Blocks.includes( this.context ) ) {
+ selectors.push( '#express-payment-method-ppcp-googlepay' );
+ }
+
+ // Classic Checkout: Google Pay gateway.
+ if ( CONTEXT.Gateways === this.context ) {
+ selectors.push(
+ '.wc_payment_method.payment_method_ppcp-googlepay'
+ );
+ }
+
+ this.log( 'Wrapper Elements:', selectors );
+ return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) =>
+ Array.from( document.querySelectorAll( selector ) )
+ );
+ }
+
+ /**
+ * Checks whether the main button-wrapper is present in the current DOM.
+ *
+ * @return {boolean} True, if the button context (wrapper element) is found.
+ */
+ get isPresent() {
+ return this.wrapperElement instanceof HTMLElement;
+ }
+
init( config, transactionInfo ) {
- if ( this.isInitialized ) {
+ if ( this.#isInitialized ) {
return;
}
- this.isInitialized = true;
if ( ! this.validateConfig() ) {
return;
@@ -85,6 +281,7 @@ class GooglepayButton {
}
this.log( 'Init' );
+ this.#isInitialized = true;
this.googlePayConfig = config;
this.transactionInfo = transactionInfo;
@@ -103,40 +300,7 @@ class GooglepayButton {
)
.then( ( response ) => {
if ( response.result ) {
- if (
- ( this.context === 'checkout' ||
- this.context === 'pay-now' ) &&
- this.buttonConfig.is_wc_gateway_enabled === '1'
- ) {
- const wrapper = document.getElementById(
- 'ppc-button-ppcp-googlepay'
- );
-
- if ( wrapper ) {
- const { ppcpStyle, buttonStyle } =
- this.contextConfig();
-
- wrapper.classList.add(
- `ppcp-button-${ ppcpStyle.shape }`,
- 'ppcp-button-apm',
- 'ppcp-button-googlepay'
- );
-
- if ( ppcpStyle.height ) {
- wrapper.style.height = `${ ppcpStyle.height }px`;
- }
-
- this.addButtonCheckout(
- this.baseCardPaymentMethod,
- wrapper,
- buttonStyle
- );
-
- return;
- }
- }
-
- this.addButton( this.baseCardPaymentMethod );
+ this.addButton();
}
} )
.catch( function ( err ) {
@@ -149,7 +313,7 @@ class GooglepayButton {
return;
}
- this.isInitialized = false;
+ this.#isInitialized = false;
this.init( this.googlePayConfig, this.transactionInfo );
}
@@ -177,39 +341,6 @@ class GooglepayButton {
return true;
}
- /**
- * Returns configurations relative to this button context.
- */
- contextConfig() {
- const config = {
- wrapper: this.buttonConfig.button.wrapper,
- ppcpStyle: this.ppcpConfig.button.style,
- buttonStyle: this.buttonConfig.button.style,
- ppcpButtonWrapper: this.ppcpConfig.button.wrapper,
- };
-
- if ( this.context === 'mini-cart' ) {
- config.wrapper = this.buttonConfig.button.mini_cart_wrapper;
- config.ppcpStyle = this.ppcpConfig.button.mini_cart_style;
- config.buttonStyle = this.buttonConfig.button.mini_cart_style;
- config.ppcpButtonWrapper = this.ppcpConfig.button.mini_cart_wrapper;
-
- // Handle incompatible types.
- if ( config.buttonStyle.type === 'buy' ) {
- config.buttonStyle.type = 'pay';
- }
- }
-
- if (
- [ 'cart-block', 'checkout-block' ].indexOf( this.context ) !== -1
- ) {
- config.ppcpButtonWrapper =
- '#express-payment-method-ppcp-gateway-paypal';
- }
-
- return config;
- }
-
initClient() {
const callbacks = {
onPaymentAuthorized: this.onPaymentAuthorized.bind( this ),
@@ -235,7 +366,8 @@ class GooglepayButton {
}
initEventHandlers() {
- const { wrapper, ppcpButtonWrapper } = this.contextConfig();
+ const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
+ const wrapper = `#${ this.wrapperId }`;
if ( wrapper === ppcpButtonWrapper ) {
throw new Error(
@@ -272,77 +404,104 @@ class GooglepayButton {
/**
* Add a Google Pay purchase button
- * @param baseCardPaymentMethod
*/
- addButton( baseCardPaymentMethod ) {
+ addButton() {
this.log( 'addButton' );
- const { wrapper, ppcpStyle, buttonStyle } = this.contextConfig();
+ const insertButton = () => {
+ const wrapper = this.wrapperElement;
+ const baseCardPaymentMethod = this.baseCardPaymentMethod;
+ const { color, type, language } = this.buttonStyle;
+ const { shape, height } = this.ppcpStyle;
- this.waitForWrapper( wrapper, () => {
- // Prevent duplicate payment buttons.
- this.removeButton();
+ wrapper.classList.add(
+ `ppcp-button-${ shape }`,
+ 'ppcp-button-apm',
+ 'ppcp-button-googlepay'
+ );
- jQuery( wrapper ).addClass( 'ppcp-button-' + ppcpStyle.shape );
-
- if ( ppcpStyle.height ) {
- jQuery( wrapper ).css( 'height', `${ ppcpStyle.height }px` );
+ if ( height ) {
+ wrapper.style.height = `${ height }px`;
}
/**
* @see https://developers.google.com/pay/api/web/reference/client#createButton
*/
- this.#button = this.paymentsClient.createButton( {
+ const button = this.paymentsClient.createButton( {
onClick: this.onButtonClick.bind( this ),
allowedPaymentMethods: [ baseCardPaymentMethod ],
- buttonColor: buttonStyle.color || 'black',
- buttonType: buttonStyle.type || 'pay',
- buttonLocale: buttonStyle.language || 'en',
+ buttonColor: color || 'black',
+ buttonType: type || 'pay',
+ buttonLocale: language || 'en',
buttonSizeMode: 'fill',
} );
- jQuery( wrapper ).append( this.#button );
+ this.log( 'Insert Button', { wrapper, button } );
+
+ wrapper.replaceChildren( button );
+ this.show();
+ };
+
+ this.waitForWrapper( insertButton );
+ }
+
+ waitForWrapper( callback, delay = 100, timeout = 2000 ) {
+ let interval = 0;
+ const startTime = Date.now();
+
+ const stop = () => {
+ if ( interval ) {
+ clearInterval( interval );
+ }
+ interval = 0;
+ };
+
+ const checkElement = () => {
+ if ( this.isPresent ) {
+ stop();
+ callback();
+ return;
+ }
+
+ const timeElapsed = Date.now() - startTime;
+
+ if ( timeElapsed > timeout ) {
+ stop();
+ this.log( '!! Wrapper not found:', this.wrapperId );
+ }
+ };
+
+ interval = setInterval( checkElement, delay );
+ }
+
+ /**
+ * Hides all wrappers that belong to this GooglePayButton instance.
+ */
+ hide() {
+ this.log( 'Hide' );
+ this.allElements.forEach( ( element ) => {
+ element.style.display = 'none';
} );
}
/**
- * Removes the payment button that was injected via addButton()
+ * Ensures all wrapper elements of this GooglePayButton instance are visible.
*/
- removeButton() {
- if ( ! this.#button ) {
+ show() {
+ if ( ! this.isPresent ) {
+ this.log( 'Cannot show button, wrapper is not present' );
return;
}
+ this.log( 'Show' );
- this.#button.remove();
- this.#button = null;
- }
+ // Classic Checkout: Make the Google Pay gateway visible.
+ document
+ .querySelectorAll( 'style#ppcp-hide-google-pay' )
+ .forEach( ( el ) => el.remove() );
- addButtonCheckout( baseCardPaymentMethod, wrapper, buttonStyle ) {
- const button = this.paymentsClient.createButton( {
- onClick: this.onButtonClick.bind( this ),
- allowedPaymentMethods: [ baseCardPaymentMethod ],
- buttonColor: buttonStyle.color || 'black',
- buttonType: buttonStyle.type || 'pay',
- buttonLocale: buttonStyle.language || 'en',
- buttonSizeMode: 'fill',
+ this.allElements.forEach( ( element ) => {
+ element.style.display = 'block';
} );
-
- wrapper.appendChild( button );
- }
-
- waitForWrapper( selector, callback, delay = 100, timeout = 2000 ) {
- const startTime = Date.now();
- const interval = setInterval( () => {
- const el = document.querySelector( selector );
- const timeElapsed = Date.now() - startTime;
-
- if ( el ) {
- clearInterval( interval );
- callback( el );
- } else if ( timeElapsed > timeout ) {
- clearInterval( interval );
- }
- }, delay );
}
//------------------------
diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php
index bd6faea79..98bff2c97 100644
--- a/modules/ppcp-googlepay/src/Assets/Button.php
+++ b/modules/ppcp-googlepay/src/Assets/Button.php
@@ -290,6 +290,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
+ $this->hide_gateway_until_eligible();
},
21
);
@@ -303,6 +304,7 @@ class Button implements ButtonInterface {
$render_placeholder,
function () {
$this->googlepay_button();
+ $this->hide_gateway_until_eligible();
},
21
);
@@ -335,6 +337,21 @@ class Button implements ButtonInterface {
+
+
Date: Tue, 30 Jul 2024 12:50:41 +0200
Subject: [PATCH 05/40] =?UTF-8?q?=E2=9C=A8=20Add=20new=20isEligible=20flag?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The flag is set once Google’s PaymentClient responds to the isReadyToPay() request and controls the rendering of the button
---
.../resources/js/GooglepayButton.js | 133 +++++++++++++-----
1 file changed, 95 insertions(+), 38 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 9571e9735..364a6f4e3 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -39,10 +39,8 @@ const CONTEXT = {
MiniCart: 'mini-cart',
BlockCart: 'cart-block',
BlockCheckout: 'checkout-block',
- Preview: 'preview',
- // Block editor contexts.
- Blocks: [ 'cart-block', 'checkout-block' ],
- // Custom gateway contexts.
+ Preview: 'preview', // Block editor contexts.
+ Blocks: [ 'cart-block', 'checkout-block' ], // Custom gateway contexts.
Gateways: [ 'checkout', 'pay-now' ],
};
@@ -57,6 +55,14 @@ class GooglepayButton {
*/
#isInitialized = false;
+ /**
+ * Whether the current client support the payment button.
+ * This state is mainly dependent on the response of `PaymentClient.isReadyToPay()`
+ *
+ * @type {boolean}
+ */
+ #isEligible = false;
+
/**
* Client reference, provided by the Google Pay JS SDK.
* @see https://developers.google.com/pay/api/web/reference/client
@@ -267,6 +273,29 @@ class GooglepayButton {
return this.wrapperElement instanceof HTMLElement;
}
+ /**
+ * Whether the browser can accept Google Pay payments.
+ *
+ * @return {boolean} True, if payments are technically possible.
+ */
+ get isEligible() {
+ return this.#isEligible;
+ }
+
+ /**
+ * Changes the eligibility state of this button component.
+ *
+ * @param {boolean} newState Whether the browser can accept payments.
+ */
+ set isEligible( newState ) {
+ if ( newState === this.#isEligible ) {
+ return;
+ }
+
+ this.#isEligible = newState;
+ this.refresh();
+ }
+
init( config, transactionInfo ) {
if ( this.#isInitialized ) {
return;
@@ -299,12 +328,22 @@ class GooglepayButton {
)
)
.then( ( response ) => {
- if ( response.result ) {
- this.addButton();
- }
+ this.log( 'PaymentsClient.isReadyToPay response:', response );
+
+ /**
+ * In case the button wrapper element is not present in the DOM yet, wait for it
+ * to appear. Only proceed, if a button wrapper is found on this page.
+ *
+ * Not sure if this is needed, or if we can directly test for `this.isPresent`
+ * without any delay.
+ */
+ this.waitForWrapper( () => {
+ this.isEligible = !! response.result;
+ } );
} )
- .catch( function ( err ) {
+ .catch( ( err ) => {
console.error( err );
+ this.isEligible = false;
} );
}
@@ -397,6 +436,8 @@ class GooglepayButton {
}
buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) {
+ this.log( 'Ready To Pay request', baseRequest, allowedPaymentMethods );
+
return Object.assign( {}, baseRequest, {
allowedPaymentMethods,
} );
@@ -408,43 +449,47 @@ class GooglepayButton {
addButton() {
this.log( 'addButton' );
- const insertButton = () => {
- const wrapper = this.wrapperElement;
- const baseCardPaymentMethod = this.baseCardPaymentMethod;
- const { color, type, language } = this.buttonStyle;
- const { shape, height } = this.ppcpStyle;
+ const wrapper = this.wrapperElement;
+ const baseCardPaymentMethod = this.baseCardPaymentMethod;
+ const { color, type, language } = this.buttonStyle;
+ const { shape, height } = this.ppcpStyle;
- wrapper.classList.add(
- `ppcp-button-${ shape }`,
- 'ppcp-button-apm',
- 'ppcp-button-googlepay'
- );
+ wrapper.classList.add(
+ `ppcp-button-${ shape }`,
+ 'ppcp-button-apm',
+ 'ppcp-button-googlepay'
+ );
- if ( height ) {
- wrapper.style.height = `${ height }px`;
- }
+ if ( height ) {
+ wrapper.style.height = `${ height }px`;
+ }
- /**
- * @see https://developers.google.com/pay/api/web/reference/client#createButton
- */
- const button = this.paymentsClient.createButton( {
- onClick: this.onButtonClick.bind( this ),
- allowedPaymentMethods: [ baseCardPaymentMethod ],
- buttonColor: color || 'black',
- buttonType: type || 'pay',
- buttonLocale: language || 'en',
- buttonSizeMode: 'fill',
- } );
+ /**
+ * @see https://developers.google.com/pay/api/web/reference/client#createButton
+ */
+ const button = this.paymentsClient.createButton( {
+ onClick: this.onButtonClick.bind( this ),
+ allowedPaymentMethods: [ baseCardPaymentMethod ],
+ buttonColor: color || 'black',
+ buttonType: type || 'pay',
+ buttonLocale: language || 'en',
+ buttonSizeMode: 'fill',
+ } );
- this.log( 'Insert Button', { wrapper, button } );
+ this.log( 'Insert Button', { wrapper, button } );
- wrapper.replaceChildren( button );
- this.show();
- };
-
- this.waitForWrapper( insertButton );
+ wrapper.replaceChildren( button );
}
+ /**
+ * Waits for the current button's wrapper element to become available in the DOM.
+ *
+ * Not sure if still needed, or if a simple `this.isPresent` check is sufficient.
+ *
+ * @param {Function} callback Function to call when the wrapper element was detected. Only called on success.
+ * @param {number} delay Optional. Polling interval to inspect the DOM. Default to 0.1 sec
+ * @param {number} timeout Optional. Max timeout in ms. Defaults to 2 sec
+ */
waitForWrapper( callback, delay = 100, timeout = 2000 ) {
let interval = 0;
const startTime = Date.now();
@@ -474,6 +519,18 @@ class GooglepayButton {
interval = setInterval( checkElement, delay );
}
+ /**
+ * Refreshes the payment button on the page.
+ */
+ refresh() {
+ if ( this.isEligible && this.isPresent ) {
+ this.show();
+ this.addButton();
+ } else {
+ this.hide();
+ }
+ }
+
/**
* Hides all wrappers that belong to this GooglePayButton instance.
*/
From 490cd1958b7b64f75765862c4c7aef67b5239fa0 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 30 Jul 2024 13:51:16 +0200
Subject: [PATCH 06/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20config=20obje?=
=?UTF-8?q?ct=20to=20appropriate=20file?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../js/modules/Helper/CheckoutMethodState.js | 24 +++++++++++++++++++
.../resources/js/GooglepayButton.js | 19 +--------------
2 files changed, 25 insertions(+), 18 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js
index 3e284c8ef..aecf434f4 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/CheckoutMethodState.js
@@ -6,6 +6,30 @@ export const PaymentMethods = {
GOOGLEPAY: 'ppcp-googlepay',
};
+/**
+ * List of valid context values that the button can have.
+ *
+ * The "context" describes the placement or page where a payment button might be displayed.
+ *
+ * @type {Object}
+ */
+export const PaymentContext = {
+ Product: 'product',
+ Cart: 'cart',
+ Checkout: 'checkout',
+ PayNow: 'pay-now',
+ MiniCart: 'mini-cart',
+ BlockCart: 'cart-block',
+ BlockCheckout: 'checkout-block',
+ Preview: 'preview',
+
+ // Block editor contexts.
+ Blocks: [ 'cart-block', 'checkout-block' ],
+
+ // Custom gateway contexts.
+ Gateways: [ 'checkout', 'pay-now' ],
+};
+
export const ORDER_BUTTON_SELECTOR = '#place_order';
export const getCurrentPaymentMethod = () => {
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 364a6f4e3..b682ce7d3 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -6,6 +6,7 @@ import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/But
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
+import { PaymentContext as CONTEXT } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
/**
* Plugin-specific styling.
@@ -26,24 +27,6 @@ import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper
* @property {string} language - The locale; an empty string will apply the user-agent's language.
*/
-/**
- * List of valid context values that the button can have.
- *
- * @type {Object}
- */
-const CONTEXT = {
- Product: 'product',
- Cart: 'cart',
- Checkout: 'checkout',
- PayNow: 'pay-now',
- MiniCart: 'mini-cart',
- BlockCart: 'cart-block',
- BlockCheckout: 'checkout-block',
- Preview: 'preview', // Block editor contexts.
- Blocks: [ 'cart-block', 'checkout-block' ], // Custom gateway contexts.
- Gateways: [ 'checkout', 'pay-now' ],
-};
-
class GooglepayButton {
#wrapperId = '';
#ppcpButtonWrapperId = '';
From 0888c696ff5fbaae6bb75ee66d16c6b820ca8510 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 30 Jul 2024 13:55:24 +0200
Subject: [PATCH 07/40] =?UTF-8?q?=E2=9C=A8=20Sync=20gateway=20visibility?=
=?UTF-8?q?=20via=20custom=20event?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ContextBootstrap/CheckoutBootstap.js | 21 +++++-
.../resources/js/GooglepayButton.js | 69 ++++++++++++-------
2 files changed, 65 insertions(+), 25 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
index 33d1ecfd3..7a016fd9e 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
@@ -68,6 +68,7 @@ class CheckoutBootstap {
jQuery( document.body ).on(
'updated_checkout payment_method_selected',
() => {
+ this.invalidatePaymentMethods();
this.updateUi();
}
);
@@ -174,6 +175,14 @@ class CheckoutBootstap {
);
}
+ invalidatePaymentMethods() {
+ /**
+ * Custom JS event to notify other modules that the payment button on the checkout page
+ * has become irrelevant or invalid.
+ */
+ document.body.dispatchEvent( new Event( 'ppcp_invalidate_methods' ) );
+ }
+
updateUi() {
const currentPaymentMethod = getCurrentPaymentMethod();
const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL;
@@ -232,9 +241,17 @@ class CheckoutBootstap {
}
}
- setVisible( '#ppc-button-ppcp-googlepay', isGooglePayMethod );
+ /**
+ * Custom JS event that is observed by the relevant payment gateway.
+ *
+ * Dynamic part of the event name is the payment method ID, for example
+ * "ppcp-credit-card-gateway" or "ppcp-googlepay"
+ */
+ document.body.dispatchEvent(
+ new Event( `ppcp_render_method-${ currentPaymentMethod }` )
+ );
- jQuery( document.body ).trigger( 'ppcp_checkout_rendered' );
+ document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) );
}
shouldShowMessages() {
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index b682ce7d3..e6aa1851b 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -6,7 +6,10 @@ import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/But
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
-import { PaymentContext as CONTEXT } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
+import {
+ PaymentMethods,
+ PaymentContext as CONTEXT,
+} from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
/**
* Plugin-specific styling.
@@ -69,6 +72,10 @@ class GooglepayButton {
this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
+ this.hide = this.hide.bind( this );
+ this.show = this.show.bind( this );
+ this.refresh = this.refresh.bind( this );
+
this.log( 'Create instance' );
}
@@ -388,34 +395,50 @@ class GooglepayButton {
}
initEventHandlers() {
- const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
- const wrapper = `#${ this.wrapperId }`;
-
- if ( wrapper === ppcpButtonWrapper ) {
- throw new Error(
- `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
+ if ( CONTEXT.Gateways.includes( this.context ) ) {
+ document.body.addEventListener(
+ 'ppcp_invalidate_methods',
+ this.hide
);
- }
- const syncButtonVisibility = () => {
- const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
- setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
- setEnabled(
- wrapper,
- ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
+ document.body.addEventListener(
+ `ppcp_render_method-${ PaymentMethods.GOOGLEPAY }`,
+ this.refresh
);
- };
+ } else {
+ /**
+ * Review: The following logic appears to be unnecessary. Is it still required?
+ */
- jQuery( document ).on(
- 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
- ( ev, data ) => {
- if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
- syncButtonVisibility();
- }
+ const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
+ const wrapper = `#${ this.wrapperId }`;
+
+ if ( wrapper === ppcpButtonWrapper ) {
+ throw new Error(
+ `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
+ );
}
- );
- syncButtonVisibility();
+ const syncButtonVisibility = () => {
+ const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
+ setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
+ setEnabled(
+ wrapper,
+ ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
+ );
+ };
+
+ jQuery( document ).on(
+ 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
+ ( ev, data ) => {
+ if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
+ syncButtonVisibility();
+ }
+ }
+ );
+
+ syncButtonVisibility();
+ }
}
buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) {
From 9da37a2cc68d4ab0ed1326d19af80cb21d1cb453 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 31 Jul 2024 10:01:42 +0200
Subject: [PATCH 08/40] =?UTF-8?q?=E2=9C=A8=20Introduce=20new=20=E2=80=9Cis?=
=?UTF-8?q?Visible=E2=80=9D=20flag?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/GooglepayButton.js | 171 ++++++++++--------
1 file changed, 97 insertions(+), 74 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index e6aa1851b..0ec6d494f 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,8 +1,5 @@
/* global google */
-/* global jQuery */
-import { setVisible } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
-import { setEnabled } from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
@@ -49,6 +46,13 @@ class GooglepayButton {
*/
#isEligible = false;
+ /**
+ * Whether this button is visible. Modified by `show()` and `hide()`
+ *
+ * @type {boolean}
+ */
+ #isVisible = false;
+
/**
* Client reference, provided by the Google Pay JS SDK.
* @see https://developers.google.com/pay/api/web/reference/client
@@ -72,8 +76,6 @@ class GooglepayButton {
this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
- this.hide = this.hide.bind( this );
- this.show = this.show.bind( this );
this.refresh = this.refresh.bind( this );
this.log( 'Create instance' );
@@ -263,6 +265,34 @@ class GooglepayButton {
return this.wrapperElement instanceof HTMLElement;
}
+ /**
+ * The visibility state of the button.
+ * This flag does not reflect actual visibility on the page, but rather, if the button
+ * is intended/allowed to be displayed, in case all other checks pass.
+ *
+ * @return {boolean} True indicates, that the button can be displayed
+ */
+ get isVisible() {
+ return this.#isVisible;
+ }
+
+ /**
+ * Change the visibility of the button.
+ *
+ * A visible button does not always force the button to render on the page. It only means, that
+ * the button is allowed or not allowed to render, if certain other conditions are met.
+ *
+ * @param {boolean} newState Whether rendering the button is allowed.
+ */
+ set isVisible( newState ) {
+ if ( this.#isVisible === newState ) {
+ return;
+ }
+
+ this.#isVisible = newState;
+ this.refresh();
+ }
+
/**
* Whether the browser can accept Google Pay payments.
*
@@ -396,48 +426,46 @@ class GooglepayButton {
initEventHandlers() {
if ( CONTEXT.Gateways.includes( this.context ) ) {
- document.body.addEventListener(
- 'ppcp_invalidate_methods',
- this.hide
- );
+ document.body.addEventListener( 'ppcp_invalidate_methods', () => {
+ this.isVisible = false;
+ } );
document.body.addEventListener(
`ppcp_render_method-${ PaymentMethods.GOOGLEPAY }`,
- this.refresh
+ () => {
+ this.isVisible = true;
+ }
);
} else {
/**
* Review: The following logic appears to be unnecessary. Is it still required?
+ * /
+ const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
+ const wrapper = `#${ this.wrapperId }`;
+ if ( wrapper === ppcpButtonWrapper ) {
+ throw new Error(
+ `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
+ );
+ }
+ const syncButtonVisibility = () => {
+ const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
+ setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
+ setEnabled(
+ wrapper,
+ ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
+ );
+ };
+ jQuery( document ).on(
+ 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
+ ( ev, data ) => {
+ if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
+ syncButtonVisibility();
+ }
+ }
+ );
+ syncButtonVisibility();
+ //
*/
-
- const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
- const wrapper = `#${ this.wrapperId }`;
-
- if ( wrapper === ppcpButtonWrapper ) {
- throw new Error(
- `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
- );
- }
-
- const syncButtonVisibility = () => {
- const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
- setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
- setEnabled(
- wrapper,
- ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
- );
- };
-
- jQuery( document ).on(
- 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
- ( ev, data ) => {
- if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
- syncButtonVisibility();
- }
- }
- );
-
- syncButtonVisibility();
}
}
@@ -482,7 +510,10 @@ class GooglepayButton {
buttonSizeMode: 'fill',
} );
- this.log( 'Insert Button', { wrapper, button } );
+ this.log( 'Insert Button', {
+ wrapper,
+ button,
+ } );
wrapper.replaceChildren( button );
}
@@ -492,7 +523,8 @@ class GooglepayButton {
*
* Not sure if still needed, or if a simple `this.isPresent` check is sufficient.
*
- * @param {Function} callback Function to call when the wrapper element was detected. Only called on success.
+ * @param {Function} callback Function to call when the wrapper element was detected. Only
+ * called on success.
* @param {number} delay Optional. Polling interval to inspect the DOM. Default to 0.1 sec
* @param {number} timeout Optional. Max timeout in ms. Defaults to 2 sec
*/
@@ -529,44 +561,35 @@ class GooglepayButton {
* Refreshes the payment button on the page.
*/
refresh() {
- if ( this.isEligible && this.isPresent ) {
- this.show();
+ const showButtonWrapper = () => {
+ this.log( 'Show' );
+
+ // Classic Checkout: Make the Google Pay gateway visible.
+ document
+ .querySelectorAll( 'style#ppcp-hide-google-pay' )
+ .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';
+ } );
+ };
+
+ if ( this.isVisible && this.isEligible && this.isPresent ) {
+ showButtonWrapper();
this.addButton();
} else {
- this.hide();
+ hideButtonWrapper();
}
}
- /**
- * Hides all wrappers that belong to this GooglePayButton instance.
- */
- hide() {
- this.log( 'Hide' );
- this.allElements.forEach( ( element ) => {
- element.style.display = 'none';
- } );
- }
-
- /**
- * Ensures all wrapper elements of this GooglePayButton instance are visible.
- */
- show() {
- if ( ! this.isPresent ) {
- this.log( 'Cannot show button, wrapper is not present' );
- return;
- }
- this.log( 'Show' );
-
- // Classic Checkout: Make the Google Pay gateway visible.
- document
- .querySelectorAll( 'style#ppcp-hide-google-pay' )
- .forEach( ( el ) => el.remove() );
-
- this.allElements.forEach( ( element ) => {
- element.style.display = 'block';
- } );
- }
-
//------------------------
// Button click
//------------------------
From f1f243505ce3fe2bcf2f21ebb7bf42523f987e9c Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 2 Aug 2024 16:32:27 +0200
Subject: [PATCH 09/40] =?UTF-8?q?=E2=9C=A8=20Introduce=20a=20new=20Console?=
=?UTF-8?q?Logger=20JS=20class?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Extract debug logic to separate component
---
.../js/modules/Helper/ConsoleLogger.js | 42 ++++++++++++++++++
.../resources/js/GooglepayButton.js | 43 ++++++-------------
2 files changed, 56 insertions(+), 29 deletions(-)
create mode 100644 modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js b/modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js
new file mode 100644
index 000000000..689f07c0a
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js
@@ -0,0 +1,42 @@
+/**
+ * Logs debug details to the console.
+ *
+ * A utility class that is used by payment buttons on the front-end, like the GooglePayButton.
+ */
+export default class ConsoleLogger {
+ /**
+ * The prefix to display before every log output.
+ *
+ * @type {string}
+ */
+ #prefix = '';
+
+ /**
+ * Whether logging is enabled, disabled by default.
+ *
+ * @type {boolean}
+ */
+ #enabled = false;
+
+ constructor( ...prefixes ) {
+ if ( prefixes.length ) {
+ this.#prefix = `[${ prefixes.join( ' | ' ) }]`;
+ }
+ }
+
+ set enabled( state ) {
+ this.#enabled = state;
+ }
+
+ log( ...args ) {
+ if ( this.#enabled ) {
+ console.log( this.#prefix, ...args );
+ }
+ }
+
+ error( ...args ) {
+ if ( this.#enabled ) {
+ console.error( this.#prefix, ...args );
+ }
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 0ec6d494f..a21f803a4 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,5 +1,6 @@
/* global google */
+import ConsoleLogger from '../../../ppcp-button/resources/js/modules/Helper/ConsoleLogger';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
@@ -28,6 +29,11 @@ import {
*/
class GooglepayButton {
+ /**
+ * @type {ConsoleLogger}
+ */
+ #logger;
+
#wrapperId = '';
#ppcpButtonWrapperId = '';
@@ -66,7 +72,8 @@ class GooglepayButton {
ppcpConfig,
contextHandler
) {
- this._initDebug( !! buttonConfig?.is_debug, context );
+ this.#logger = new ConsoleLogger( 'GooglePayButton', context );
+ this.#logger.enabled = !! buttonConfig?.is_debug;
apmButtonsInit( ppcpConfig );
@@ -81,34 +88,12 @@ class GooglepayButton {
this.log( 'Create instance' );
}
- /**
- * NOOP log function to avoid errors when debugging is disabled.
- */
- log() {}
+ log( ...args ) {
+ this.#logger.log( ...args );
+ }
- /**
- * Enables debugging tools, when the button's is_debug flag is set.
- *
- * @param {boolean} enableDebugging If debugging features should be enabled for this instance.
- * @param {string} context Used to make the instance accessible via the global debug
- * object.
- * @private
- */
- _initDebug( enableDebugging, context ) {
- if ( ! enableDebugging || this.#isInitialized ) {
- return;
- }
-
- document.ppcpGooglepayButtons = document.ppcpGooglepayButtons || {};
- document.ppcpGooglepayButtons[ context ] = this;
-
- this.log = ( ...args ) => {
- console.log( `[GooglePayButton | ${ context }]`, ...args );
- };
-
- document.addEventListener( 'ppcp-googlepay-debug', () => {
- this.log( this );
- } );
+ error( ...args ) {
+ this.#logger.error( ...args );
}
/**
@@ -550,7 +535,7 @@ class GooglepayButton {
if ( timeElapsed > timeout ) {
stop();
- this.log( '!! Wrapper not found:', this.wrapperId );
+ this.error( 'Wrapper not found:', this.wrapperId );
}
};
From f69209b91ce6f4bd50bc5a17019e3bb21b84d1c6 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 2 Aug 2024 17:12:07 +0200
Subject: [PATCH 10/40] =?UTF-8?q?=E2=9C=A8=20New=20PaymentButton=20base=20?=
=?UTF-8?q?class=20for=20APM=20buttons?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This class is used to render buttons in the front end, and encapsulates logic that is shared between ApplePay and GooglePay buttons
---
.../js/modules/Renderer/PaymentButton.js | 101 ++++++++++++++++++
.../resources/js/GooglepayButton.js | 59 ++++------
2 files changed, 123 insertions(+), 37 deletions(-)
create mode 100644 modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
new file mode 100644
index 000000000..8843ca662
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -0,0 +1,101 @@
+import ConsoleLogger from '../Helper/ConsoleLogger';
+import { apmButtonsInit } from '../Helper/ApmButtons';
+
+/**
+ * Base class for APM payment buttons, like GooglePay and ApplePay.
+ *
+ * This class is not intended for the PayPal button.
+ */
+export default class PaymentButton {
+ /**
+ * @type {ConsoleLogger}
+ */
+ #logger;
+
+ /**
+ * Whether the payment button is initialized.
+ *
+ * @type {boolean}
+ */
+ #isInitialized = false;
+
+ /**
+ * The button's context.
+ */
+ #context;
+
+ #buttonConfig;
+
+ #ppcpConfig;
+
+ constructor( gatewayName, context, buttonConfig, ppcpConfig ) {
+ this.#logger = new ConsoleLogger( gatewayName, context );
+ this.#logger.enabled = !! buttonConfig?.is_debug;
+
+ this.#context = context;
+ this.#buttonConfig = buttonConfig;
+ this.#ppcpConfig = ppcpConfig;
+
+ apmButtonsInit( ppcpConfig );
+ }
+
+ /**
+ * Whether the payment button was fully initialized. Read-only.
+ *
+ * @return {boolean} True indicates, that the button was fully initialized.
+ */
+ get isInitialized() {
+ return this.#isInitialized;
+ }
+
+ /**
+ * The button's context. Read-only.
+ *
+ * TODO: Convert the string to a context-object (primitive obsession smell)
+ *
+ * @return {string} The button context.
+ */
+ get context() {
+ return this.#context;
+ }
+
+ /**
+ * Log a debug detail to the browser console.
+ *
+ * @param {any} args
+ */
+ log( ...args ) {
+ this.#logger.log( ...args );
+ }
+
+ /**
+ * Log an error message to the browser console.
+ *
+ * @param {any} args
+ */
+ error( ...args ) {
+ this.#logger.error( ...args );
+ }
+
+ /**
+ * Must be named `init()` to simulate "protected" visibility:
+ * Since the derived class also implements a method with the same name, this method can only
+ * be called by the derived class, but not from any other code.
+ *
+ * @protected
+ */
+ init() {
+ this.#isInitialized = true;
+ }
+
+ /**
+ * Must be named `reinit()` to simulate "protected" visibility:
+ * Since the derived class also implements a method with the same name, this method can only
+ * be called by the derived class, but not from any other code.
+ *
+ * @protected
+ */
+ reinit() {
+ this.#isInitialized = false;
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index a21f803a4..f778ff8e2 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,9 +1,8 @@
/* global google */
-import ConsoleLogger from '../../../ppcp-button/resources/js/modules/Helper/ConsoleLogger';
+import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
-import { apmButtonsInit } from '../../../ppcp-button/resources/js/modules/Helper/ApmButtons';
import {
PaymentMethods,
PaymentContext as CONTEXT,
@@ -28,22 +27,10 @@ import {
* @property {string} language - The locale; an empty string will apply the user-agent's language.
*/
-class GooglepayButton {
- /**
- * @type {ConsoleLogger}
- */
- #logger;
-
+class GooglepayButton extends PaymentButton {
#wrapperId = '';
#ppcpButtonWrapperId = '';
- /**
- * Whether the payment button is initialized.
- *
- * @type {boolean}
- */
- #isInitialized = false;
-
/**
* Whether the current client support the payment button.
* This state is mainly dependent on the response of `PaymentClient.isReadyToPay()`
@@ -72,12 +59,8 @@ class GooglepayButton {
ppcpConfig,
contextHandler
) {
- this.#logger = new ConsoleLogger( 'GooglePayButton', context );
- this.#logger.enabled = !! buttonConfig?.is_debug;
+ super( 'GooglePayButton', context, buttonConfig, ppcpConfig );
- apmButtonsInit( ppcpConfig );
-
- this.context = context;
this.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
@@ -88,14 +71,6 @@ class GooglepayButton {
this.log( 'Create instance' );
}
- log( ...args ) {
- this.#logger.log( ...args );
- }
-
- error( ...args ) {
- this.#logger.error( ...args );
- }
-
/**
* Determines if the current payment button should be rendered as a stand-alone gateway.
* The return value `false` usually means, that the payment button is bundled with all available
@@ -301,8 +276,21 @@ class GooglepayButton {
this.refresh();
}
- init( config, transactionInfo ) {
- if ( this.#isInitialized ) {
+ init( config = null, transactionInfo = null ) {
+ if ( this.isInitialized ) {
+ return;
+ }
+ if ( config ) {
+ this.googlePayConfig = config;
+ }
+ if ( transactionInfo ) {
+ this.transactionInfo = transactionInfo;
+ }
+
+ if ( ! this.googlePayConfig || ! this.transactionInfo ) {
+ this.error(
+ 'Init called without providing config or transactionInfo'
+ );
return;
}
@@ -314,11 +302,8 @@ class GooglepayButton {
return;
}
- this.log( 'Init' );
- this.#isInitialized = true;
+ super.init();
- this.googlePayConfig = config;
- this.transactionInfo = transactionInfo;
this.allowedPaymentMethods = config.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
@@ -353,12 +338,12 @@ class GooglepayButton {
}
reinit() {
- if ( ! this.googlePayConfig ) {
+ if ( ! this.isInitialized ) {
return;
}
- this.#isInitialized = false;
- this.init( this.googlePayConfig, this.transactionInfo );
+ super.reinit();
+ this.init();
}
validateConfig() {
From 3c200e408a8d957983046afa9870698f3f32f1fd Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Mon, 5 Aug 2024 12:54:24 +0200
Subject: [PATCH 11/40] =?UTF-8?q?=F0=9F=9A=9A=20Move=20ConsoleLogger=20to?=
=?UTF-8?q?=20wc-gateway=20module?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Renderer/PaymentButton.js | 2 +-
.../resources/js/helper}/ConsoleLogger.js | 3 ++-
.../resources/js/helper/preview-button.js | 10 ++++++----
3 files changed, 9 insertions(+), 6 deletions(-)
rename modules/{ppcp-button/resources/js/modules/Helper => ppcp-wc-gateway/resources/js/helper}/ConsoleLogger.js (88%)
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
index 8843ca662..20459f52e 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -1,4 +1,4 @@
-import ConsoleLogger from '../Helper/ConsoleLogger';
+import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
import { apmButtonsInit } from '../Helper/ApmButtons';
/**
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
similarity index 88%
rename from modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js
rename to modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
index 689f07c0a..4b8891247 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/ConsoleLogger.js
+++ b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
@@ -1,5 +1,5 @@
/**
- * Logs debug details to the console.
+ * Helper component to log debug details to the browser console.
*
* A utility class that is used by payment buttons on the front-end, like the GooglePayButton.
*/
@@ -30,6 +30,7 @@ export default class ConsoleLogger {
log( ...args ) {
if ( this.#enabled ) {
+ // eslint-disable-next-line
console.log( this.#prefix, ...args );
}
}
diff --git a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js
index 30b71f511..d9c4f8264 100644
--- a/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js
+++ b/modules/ppcp-wc-gateway/resources/js/helper/preview-button.js
@@ -1,9 +1,11 @@
+/* global jQuery */
+
/**
* Returns a Map with all input fields that are relevant to render the preview of the
* given payment button.
*
* @param {string} apmName - Value of the custom attribute `data-ppcp-apm-name`.
- * @return {Map}
+ * @return {Map} List of input elements found on the current admin page.
*/
export function getButtonFormFields( apmName ) {
const inputFields = document.querySelectorAll(
@@ -28,9 +30,9 @@ export function getButtonFormFields( apmName ) {
/**
* Returns a function that triggers an update of the specified preview button, when invoked.
-
+ *
* @param {string} apmName
- * @return {((object) => void)}
+ * @return {((object) => void)} Trigger-function; updates preview buttons when invoked.
*/
export function buttonRefreshTriggerFactory( apmName ) {
const eventName = `ppcp_paypal_render_preview_${ apmName }`;
@@ -44,7 +46,7 @@ export function buttonRefreshTriggerFactory( apmName ) {
* Returns a function that gets the current form values of the specified preview button.
*
* @param {string} apmName
- * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}}
+ * @return {() => {button: {wrapper:string, is_enabled:boolean, style:{}}}} Getter-function; returns preview config details when invoked.
*/
export function buttonSettingsGetterFactory( apmName ) {
const fields = getButtonFormFields( apmName );
From b85a16abda79b2db715f8ac0b78e43e6a261f9b1 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 6 Aug 2024 15:59:54 +0200
Subject: [PATCH 12/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20button=20even?=
=?UTF-8?q?t=20dispatcher=20to=20helper=20file?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ContextBootstrap/CheckoutBootstap.js | 13 +++--
.../js/modules/Helper/PaymentButtonHelpers.js | 48 +++++++++++++++++++
2 files changed, 57 insertions(+), 4 deletions(-)
create mode 100644 modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
index 7a016fd9e..d679a7f21 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
@@ -7,6 +7,10 @@ import {
PaymentMethods,
} from '../Helper/CheckoutMethodState';
import BootstrapHelper from '../Helper/BootstrapHelper';
+import {
+ ButtonEvents,
+ dispatchButtonEvent,
+} from '../Helper/PaymentButtonHelpers';
class CheckoutBootstap {
constructor( gateway, renderer, spinner, errorHandler ) {
@@ -180,7 +184,7 @@ class CheckoutBootstap {
* Custom JS event to notify other modules that the payment button on the checkout page
* has become irrelevant or invalid.
*/
- document.body.dispatchEvent( new Event( 'ppcp_invalidate_methods' ) );
+ dispatchButtonEvent( { event: ButtonEvents.INVALIDATE } );
}
updateUi() {
@@ -247,9 +251,10 @@ class CheckoutBootstap {
* Dynamic part of the event name is the payment method ID, for example
* "ppcp-credit-card-gateway" or "ppcp-googlepay"
*/
- document.body.dispatchEvent(
- new Event( `ppcp_render_method-${ currentPaymentMethod }` )
- );
+ dispatchButtonEvent( {
+ event: ButtonEvents.RENDER,
+ paymentMethod: currentPaymentMethod,
+ } );
document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) );
}
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
new file mode 100644
index 000000000..19ecfc001
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
@@ -0,0 +1,48 @@
+/**
+ * Helper function used by PaymentButton instances.
+ *
+ * @file
+ */
+
+/**
+ * Collection of recognized event names for payment button events.
+ *
+ * @type {Object}
+ */
+export const ButtonEvents = Object.freeze( {
+ INVALIDATE: 'ppcp_invalidate_methods',
+ RENDER: 'ppcp_render_method',
+ REDRAW: 'ppcp_redraw_method',
+} );
+
+/**
+ * Verifies if the given event name is a valid Payment Button event.
+ *
+ * @param {string} event - The event name to verify.
+ * @return {boolean} True, if the event name is valid.
+ */
+export function isValidButtonEvent( event ) {
+ const buttonEventValues = Object.values( ButtonEvents );
+
+ return buttonEventValues.includes( event );
+}
+
+/**
+ * Dispatches a payment button event.
+ *
+ * @param {Object} options - The options for dispatching the event.
+ * @param {string} options.event - Event to dispatch.
+ * @param {string} [options.paymentMethod] - Optional. Name of payment method, to target a specific button only.
+ * @throws {Error} Throws an error if the event is invalid.
+ */
+export function dispatchButtonEvent( { event, paymentMethod = '' } ) {
+ if ( ! isValidButtonEvent( event ) ) {
+ throw new Error( `Invalid event: ${ event }` );
+ }
+
+ const fullEventName = paymentMethod
+ ? `${ event }-${ paymentMethod }`
+ : event;
+
+ document.body.dispatchEvent( new Event( fullEventName ) );
+}
From fc805a4369c5e4bb35a133af765a7730e74af8d2 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 6 Aug 2024 17:45:53 +0200
Subject: [PATCH 13/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20most=20of=20t?=
=?UTF-8?q?he=20display=20logic=20to=20base=20class?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The PaymentButton base class now handles display logic that is shared between different APMs
---
.../js/modules/Helper/PaymentButtonHelpers.js | 69 +++
.../js/modules/Renderer/PaymentButton.js | 418 +++++++++++++++-
.../resources/js/GooglepayButton.js | 459 ++----------------
modules/ppcp-googlepay/src/Assets/Button.php | 7 +-
4 files changed, 539 insertions(+), 414 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
index 19ecfc001..f9a066a23 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers.js
@@ -15,6 +15,54 @@ export const ButtonEvents = Object.freeze( {
REDRAW: 'ppcp_redraw_method',
} );
+/**
+ *
+ * @param {string} defaultId - Default wrapper ID.
+ * @param {string} miniCartId - Wrapper inside the mini-cart.
+ * @param {string} smartButtonId - ID of the smart button wrapper.
+ * @param {string} blockId - Block wrapper ID (express checkout, block cart).
+ * @param {string} gatewayId - Gateway wrapper ID (classic checkout).
+ * @return {{MiniCart, Gateway, Block, SmartButton, Default}} List of all wrapper IDs, by context.
+ */
+export function combineWrapperIds(
+ defaultId = '',
+ miniCartId = '',
+ smartButtonId = '',
+ blockId = '',
+ gatewayId = ''
+) {
+ const sanitize = ( id ) => id.replace( /^#/, '' );
+
+ return {
+ Default: sanitize( defaultId ),
+ SmartButton: sanitize( smartButtonId ),
+ Block: sanitize( blockId ),
+ Gateway: sanitize( gatewayId ),
+ MiniCart: sanitize( miniCartId ),
+ };
+}
+
+/**
+ * Returns full payment button styles by combining the global ppcpConfig with
+ * payment-method-specific styling provided via buttonConfig.
+ *
+ * @param {Object} ppcpConfig - Global plugin configuration.
+ * @param {Object} buttonConfig - Payment method specific configuration.
+ * @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
+ */
+export function combineStyles( ppcpConfig, buttonConfig ) {
+ return {
+ Default: {
+ ...ppcpConfig.style,
+ ...buttonConfig.style,
+ },
+ MiniCart: {
+ ...ppcpConfig.mini_cart_style,
+ ...buttonConfig.mini_cart_style,
+ },
+ };
+}
+
/**
* Verifies if the given event name is a valid Payment Button event.
*
@@ -46,3 +94,24 @@ export function dispatchButtonEvent( { event, paymentMethod = '' } ) {
document.body.dispatchEvent( new Event( fullEventName ) );
}
+
+/**
+ * Adds an event listener for the provided button event.
+ *
+ * @param {Object} options - The options for the event listener.
+ * @param {string} options.event - Event to observe.
+ * @param {string} [options.paymentMethod] - The payment method name (optional).
+ * @param {Function} options.callback - The callback function to execute when the event is triggered.
+ * @throws {Error} Throws an error if the event is invalid.
+ */
+export function observeButtonEvent( { event, paymentMethod = '', callback } ) {
+ if ( ! isValidButtonEvent( event ) ) {
+ throw new Error( `Invalid event: ${ event }` );
+ }
+
+ const fullEventName = paymentMethod
+ ? `${ event }-${ paymentMethod }`
+ : event;
+
+ document.body.addEventListener( fullEventName, callback );
+}
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
index 20459f52e..8275d1ee4 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -1,5 +1,30 @@
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
import { apmButtonsInit } from '../Helper/ApmButtons';
+import { PaymentContext } from '../Helper/CheckoutMethodState';
+import {
+ ButtonEvents,
+ dispatchButtonEvent,
+ observeButtonEvent,
+} from '../Helper/PaymentButtonHelpers';
+
+/**
+ * Collection of all available styling options for this button.
+ *
+ * @typedef {Object} StylesCollection
+ * @property {string} Default - Default button styling.
+ * @property {string} MiniCart - Styles for mini-cart button.
+ */
+
+/**
+ * Collection of all available wrapper IDs that are possible for the button.
+ *
+ * @typedef {Object} WrapperCollection
+ * @property {string} Default - Default button wrapper.
+ * @property {string} Gateway - Wrapper for separate gateway.
+ * @property {string} Block - Wrapper for block checkout button.
+ * @property {string} MiniCart - Wrapper for mini-cart button.
+ * @property {string} SmartButton - Wrapper for smart button container.
+ */
/**
* Base class for APM payment buttons, like GooglePay and ApplePay.
@@ -12,6 +37,11 @@ export default class PaymentButton {
*/
#logger;
+ /**
+ * @type {string}
+ */
+ #methodId;
+
/**
* Whether the payment button is initialized.
*
@@ -21,27 +51,105 @@ export default class PaymentButton {
/**
* The button's context.
+ *
+ * @type {string}
*/
#context;
+ /**
+ * Object containing the IDs of all possible wrapper elements that might contain this
+ * button; only one wrapper is relevant, depending on the value of the context.
+ *
+ * @type {Object}
+ */
+ #wrappers;
+
+ /**
+ * @type {StylesCollection}
+ */
+ #styles;
+
+ /**
+ * APM relevant configuration; e.g., configuration of the GooglePay button
+ */
#buttonConfig;
+ /**
+ * Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
+ */
#ppcpConfig;
- constructor( gatewayName, context, buttonConfig, ppcpConfig ) {
- this.#logger = new ConsoleLogger( gatewayName, context );
+ /**
+ * Whether the current browser/website support the payment method.
+ *
+ * @type {boolean}
+ */
+ #isEligible = false;
+
+ /**
+ * Whether this button is visible. Modified by `show()` and `hide()`
+ *
+ * @type {boolean}
+ */
+ #isVisible = true;
+
+ /**
+ * The currently visible payment button.
+ *
+ * @see {PaymentButton.insertButton}
+ * @type {HTMLElement|null}
+ */
+ #button = null;
+
+ /**
+ * 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.
+ */
+ constructor(
+ methodId,
+ context,
+ wrappers,
+ styles,
+ buttonConfig,
+ ppcpConfig
+ ) {
+ const methodName = methodId.replace( /^ppcp?-/, '' );
+
+ this.#methodId = methodId;
+
+ this.#logger = new ConsoleLogger( methodName, context );
this.#logger.enabled = !! buttonConfig?.is_debug;
this.#context = context;
+ this.#wrappers = wrappers;
+ this.#styles = styles;
this.#buttonConfig = buttonConfig;
this.#ppcpConfig = ppcpConfig;
apmButtonsInit( ppcpConfig );
+ this.initEventListeners();
}
/**
- * Whether the payment button was fully initialized. Read-only.
+ * Internal ID of the payment gateway.
*
+ * @readonly
+ * @return {string} The internal gateway ID.
+ */
+ get methodId() {
+ return this.#methodId;
+ }
+
+ /**
+ * Whether the payment button was fully initialized.
+ *
+ * @readonly
* @return {boolean} True indicates, that the button was fully initialized.
*/
get isInitialized() {
@@ -49,16 +157,188 @@ export default class PaymentButton {
}
/**
- * The button's context. Read-only.
+ * The button's context.
*
* TODO: Convert the string to a context-object (primitive obsession smell)
*
+ * @readonly
* @return {string} The button context.
*/
get context() {
return this.#context;
}
+ /**
+ * Button wrapper details.
+ *
+ * @readonly
+ * @return {WrapperCollection} Wrapper IDs.
+ */
+ get wrappers() {
+ return this.#wrappers;
+ }
+
+ /**
+ * Returns the context-relevant button style object.
+ *
+ * @readonly
+ * @return {string} Styling options.
+ */
+ get style() {
+ if ( PaymentContext.MiniCart === this.context ) {
+ return this.#styles.MiniCart;
+ }
+
+ return this.#styles.Default;
+ }
+
+ /**
+ * Returns the context-relevant wrapper ID.
+ *
+ * @readonly
+ * @return {string} The wrapper-element's ID (without the `#` prefix).
+ */
+ get wrapperId() {
+ if ( PaymentContext.MiniCart === this.context ) {
+ return this.wrappers.MiniCart;
+ } else if ( this.isSeparateGateway ) {
+ return this.wrappers.Gateway;
+ } else if ( PaymentContext.Blocks.includes( this.context ) ) {
+ return this.wrappers.Block;
+ }
+
+ return this.wrappers.Default;
+ }
+
+ /**
+ * Determines if the current payment button should be rendered as a stand-alone gateway.
+ * The return value `false` usually means, that the payment button is bundled with all available
+ * payment buttons.
+ *
+ * The decision depends on the button context (placement) and the plugin settings.
+ *
+ * @return {boolean} True, if the current button represents a stand-alone gateway.
+ */
+ get isSeparateGateway() {
+ return (
+ this.#buttonConfig.is_wc_gateway_enabled &&
+ PaymentContext.Gateways.includes( this.context )
+ );
+ }
+
+ /**
+ * Determines if the current button instance has valid and complete configuration details.
+ * Used during initialization to decide if the button can be initialized or should be skipped.
+ *
+ * Can be implemented by the derived class.
+ *
+ * @return {boolean} True indicates the config is valid and initialization can continue.
+ */
+ get isConfigValid() {
+ return true;
+ }
+
+ /**
+ * Whether the browser can accept this payment method.
+ *
+ * @return {boolean} True, if payments are technically possible.
+ */
+ get isEligible() {
+ return this.#isEligible;
+ }
+
+ /**
+ * Changes the eligibility state of this button component.
+ *
+ * @param {boolean} newState Whether the browser can accept payments.
+ */
+ set isEligible( newState ) {
+ if ( newState === this.#isEligible ) {
+ return;
+ }
+
+ this.#isEligible = newState;
+ this.triggerRedraw();
+ }
+
+ /**
+ * The visibility state of the button.
+ * This flag does not reflect actual visibility on the page, but rather, if the button
+ * is intended/allowed to be displayed, in case all other checks pass.
+ *
+ * @return {boolean} True indicates, that the button can be displayed.
+ */
+ get isVisible() {
+ return this.#isVisible;
+ }
+
+ /**
+ * Change the visibility of the button.
+ *
+ * A visible button does not always force the button to render on the page. It only means, that
+ * the button is allowed or not allowed to render, if certain other conditions are met.
+ *
+ * @param {boolean} newState Whether rendering the button is allowed.
+ */
+ set isVisible( newState ) {
+ if ( this.#isVisible === newState ) {
+ return;
+ }
+
+ this.#isVisible = newState;
+ this.triggerRedraw();
+ }
+
+ /**
+ * Returns the HTML element that wraps the current button
+ *
+ * @readonly
+ * @return {HTMLElement|null} The wrapper element, or null.
+ */
+ get wrapperElement() {
+ return document.getElementById( this.wrapperId );
+ }
+
+ /**
+ * Checks whether the main button-wrapper is present in the current DOM.
+ *
+ * @readonly
+ * @return {boolean} True, if the button context (wrapper element) is found.
+ */
+ get isPresent() {
+ return this.wrapperElement instanceof HTMLElement;
+ }
+
+ /**
+ * Returns an array of HTMLElements that belong to the payment button.
+ *
+ * @readonly
+ * @return {HTMLElement[]} List of payment button wrapper elements.
+ */
+ get allElements() {
+ const selectors = [];
+
+ // Payment button (Pay now, smart button block)
+ selectors.push( `#${ this.wrapperId }` );
+
+ // Block Checkout: Express checkout button.
+ if ( PaymentContext.Blocks.includes( this.context ) ) {
+ selectors.push( `#${ this.wrappers.Block }` );
+ }
+
+ // Classic Checkout: Separate gateway.
+ if ( this.isSeparateGateway ) {
+ selectors.push(
+ `.wc_payment_method.payment_method_${ this.methodId }`
+ );
+ }
+
+ this.log( 'Wrapper Elements:', selectors );
+ return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) =>
+ Array.from( document.querySelectorAll( selector ) )
+ );
+ }
+
/**
* Log a debug detail to the browser console.
*
@@ -98,4 +378,134 @@ export default class PaymentButton {
reinit() {
this.#isInitialized = false;
}
+
+ triggerRedraw() {
+ dispatchButtonEvent( {
+ event: ButtonEvents.REDRAW,
+ paymentMethod: this.methodId,
+ } );
+ }
+
+ /**
+ * Attaches event listeners to show or hide the payment button when needed.
+ */
+ initEventListeners() {
+ // Refresh the button - this might show, hide or re-create the payment button.
+ observeButtonEvent( {
+ event: ButtonEvents.REDRAW,
+ paymentMethod: this.methodId,
+ callback: () => this.refresh(),
+ } );
+
+ // Events relevant for buttons inside a payment gateway.
+ if ( PaymentContext.Gateways.includes( this.context ) ) {
+ // Hide the button right after the user selected _any_ gateway.
+ observeButtonEvent( {
+ event: ButtonEvents.INVALIDATE,
+ callback: () => ( this.isVisible = false ),
+ } );
+
+ // Show the button (again) when the user selected the current gateway.
+ observeButtonEvent( {
+ event: ButtonEvents.RENDER,
+ paymentMethod: this.methodId,
+ callback: () => ( this.isVisible = true ),
+ } );
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ // Show the wrapper or gateway entry, i.e. add space for the button.
+ if ( this.isEligible && this.isPresent ) {
+ showButtonWrapper();
+ } else {
+ hideButtonWrapper();
+ }
+ }
+
+ /**
+ * Prepares the button wrapper element and inserts the provided payment button into the DOM.
+ *
+ * @param {HTMLElement} button - The button element to inject.
+ */
+ insertButton( button ) {
+ if ( ! this.isPresent ) {
+ return;
+ }
+
+ if ( this.#button ) {
+ this.#button.remove();
+ }
+
+ 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 );
+ }
+
+ /**
+ * Removes the payment button from the DOM.
+ */
+ removeButton() {
+ if ( ! this.isPresent ) {
+ return;
+ }
+
+ this.log( 'removeButton' );
+
+ if ( this.#button ) {
+ this.#button.remove();
+ }
+ this.#button = null;
+
+ const wrapper = this.wrapperElement;
+
+ wrapper.innerHTML = '';
+ }
}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index f778ff8e2..347eb819b 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -1,12 +1,13 @@
/* global google */
+import {
+ combineStyles,
+ combineWrapperIds,
+} from '../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
-import {
- PaymentMethods,
- PaymentContext as CONTEXT,
-} from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
+import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
/**
* Plugin-specific styling.
@@ -28,24 +29,6 @@ import {
*/
class GooglepayButton extends PaymentButton {
- #wrapperId = '';
- #ppcpButtonWrapperId = '';
-
- /**
- * Whether the current client support the payment button.
- * This state is mainly dependent on the response of `PaymentClient.isReadyToPay()`
- *
- * @type {boolean}
- */
- #isEligible = false;
-
- /**
- * Whether this button is visible. Modified by `show()` and `hide()`
- *
- * @type {boolean}
- */
- #isVisible = false;
-
/**
* Client reference, provided by the Google Pay JS SDK.
* @see https://developers.google.com/pay/api/web/reference/client
@@ -59,221 +42,54 @@ class GooglepayButton extends PaymentButton {
ppcpConfig,
contextHandler
) {
- super( 'GooglePayButton', context, buttonConfig, ppcpConfig );
+ const wrappers = combineWrapperIds(
+ buttonConfig.button.wrapper,
+ buttonConfig.button.mini_cart_wrapper,
+ ppcpConfig.button.wrapper,
+ 'express-payment-method-ppcp-googlepay',
+ 'ppc-button-ppcp-googlepay'
+ );
+
+ console.log( ppcpConfig.button, buttonConfig.button );
+
+ 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.externalHandler = externalHandler;
this.buttonConfig = buttonConfig;
- this.ppcpConfig = ppcpConfig;
this.contextHandler = contextHandler;
- this.refresh = this.refresh.bind( this );
-
this.log( 'Create instance' );
}
/**
- * Determines if the current payment button should be rendered as a stand-alone gateway.
- * The return value `false` usually means, that the payment button is bundled with all available
- * payment buttons.
- *
- * The decision depends on the button context (placement) and the plugin settings.
- *
- * @return {boolean} True, if the current button represents a stand-alone gateway.
+ * @inheritDoc
*/
- get isSeparateGateway() {
- return (
- this.buttonConfig.is_wc_gateway_enabled &&
- CONTEXT.Gateways.includes( this.context )
- );
- }
+ get isConfigValid() {
+ const validEnvs = [ 'PRODUCTION', 'TEST' ];
- /**
- * Returns the wrapper ID for the current button context.
- * The ID varies for the MiniCart context.
- *
- * @return {string} The wrapper-element's ID (without the `#` prefix).
- */
- get wrapperId() {
- if ( ! this.#wrapperId ) {
- let id;
-
- if ( CONTEXT.MiniCart === this.context ) {
- id = this.buttonConfig.button.mini_cart_wrapper;
- } else if ( this.isSeparateGateway ) {
- id = 'ppc-button-ppcp-googlepay';
- } else {
- id = this.buttonConfig.button.wrapper;
- }
-
- this.#wrapperId = id.replace( /^#/, '' );
+ if ( ! validEnvs.includes( this.buttonConfig.environment ) ) {
+ this.error( 'Invalid environment.', this.buttonConfig.environment );
+ return false;
}
- return this.#wrapperId;
- }
-
- /**
- * Returns the wrapper ID for the ppcpButton
- *
- * @return {string} The wrapper-element's ID (without the `#` prefix).
- */
- get ppcpButtonWrapperId() {
- if ( ! this.#ppcpButtonWrapperId ) {
- let id;
-
- if ( CONTEXT.MiniCart === this.context ) {
- id = this.ppcpConfig.button.mini_cart_wrapper;
- } else if ( CONTEXT.Blocks.includes( this.context ) ) {
- id = 'express-payment-method-ppcp-gateway-paypal';
- } else {
- id = this.ppcpConfig.button.wrapper;
- }
-
- this.#ppcpButtonWrapperId = id.replace( /^#/, '' );
+ if ( ! typeof this.contextHandler?.validateContext() ) {
+ this.error( 'Invalid context handler.', this.contextHandler );
+ return false;
}
- return this.#ppcpButtonWrapperId;
- }
-
- /**
- * Returns the context-relevant PPCP style object.
- * The style for the MiniCart context can be different.
- *
- * The PPCP style are custom style options, that are provided by this plugin.
- *
- * @return {PPCPStyle} The style object.
- */
- get ppcpStyle() {
- if ( CONTEXT.MiniCart === this.context ) {
- return this.ppcpConfig.button.mini_cart_style;
- }
-
- return this.ppcpConfig.button.style;
- }
-
- /**
- * Returns default style options that are propagated to and rendered by the Google Pay button.
- *
- * These styles are the official style options provided by the Google Pay SDK.
- *
- * @return {GooglePayStyle} The style object.
- */
- get buttonStyle() {
- let style;
-
- if ( CONTEXT.MiniCart === this.context ) {
- style = this.buttonConfig.button.mini_cart_style;
-
- // Handle incompatible types.
- if ( style.type === 'buy' ) {
- style.type = 'pay';
- }
- } else {
- style = this.buttonConfig.button.style;
- }
-
- return {
- type: style.type,
- language: style.language,
- color: style.color,
- };
- }
-
- /**
- * Returns the HTML element that wraps the current button
- *
- * @return {HTMLElement|null} The wrapper element, or null.
- */
- get wrapperElement() {
- return document.getElementById( this.wrapperId );
- }
-
- /**
- * Returns an array of HTMLElements that belong to the payment button.
- *
- * @return {HTMLElement[]} List of payment button wrapper elements.
- */
- get allElements() {
- const selectors = [];
-
- // Payment button (Pay now, smart button block)
- selectors.push( `#${ this.wrapperId }` );
-
- // Block Checkout: Express checkout button.
- if ( CONTEXT.Blocks.includes( this.context ) ) {
- selectors.push( '#express-payment-method-ppcp-googlepay' );
- }
-
- // Classic Checkout: Google Pay gateway.
- if ( CONTEXT.Gateways === this.context ) {
- selectors.push(
- '.wc_payment_method.payment_method_ppcp-googlepay'
- );
- }
-
- this.log( 'Wrapper Elements:', selectors );
- return /** @type {HTMLElement[]} */ selectors.flatMap( ( selector ) =>
- Array.from( document.querySelectorAll( selector ) )
- );
- }
-
- /**
- * Checks whether the main button-wrapper is present in the current DOM.
- *
- * @return {boolean} True, if the button context (wrapper element) is found.
- */
- get isPresent() {
- return this.wrapperElement instanceof HTMLElement;
- }
-
- /**
- * The visibility state of the button.
- * This flag does not reflect actual visibility on the page, but rather, if the button
- * is intended/allowed to be displayed, in case all other checks pass.
- *
- * @return {boolean} True indicates, that the button can be displayed
- */
- get isVisible() {
- return this.#isVisible;
- }
-
- /**
- * Change the visibility of the button.
- *
- * A visible button does not always force the button to render on the page. It only means, that
- * the button is allowed or not allowed to render, if certain other conditions are met.
- *
- * @param {boolean} newState Whether rendering the button is allowed.
- */
- set isVisible( newState ) {
- if ( this.#isVisible === newState ) {
- return;
- }
-
- this.#isVisible = newState;
- this.refresh();
- }
-
- /**
- * Whether the browser can accept Google Pay payments.
- *
- * @return {boolean} True, if payments are technically possible.
- */
- get isEligible() {
- return this.#isEligible;
- }
-
- /**
- * Changes the eligibility state of this button component.
- *
- * @param {boolean} newState Whether the browser can accept payments.
- */
- set isEligible( newState ) {
- if ( newState === this.#isEligible ) {
- return;
- }
-
- this.#isEligible = newState;
- this.refresh();
+ return true;
}
init( config = null, transactionInfo = null ) {
@@ -288,27 +104,24 @@ class GooglepayButton extends PaymentButton {
}
if ( ! this.googlePayConfig || ! this.transactionInfo ) {
- this.error(
- 'Init called without providing config or transactionInfo'
- );
+ this.error( 'Missing config or transactionInfo during init.' );
return;
}
- if ( ! this.validateConfig() ) {
+ if ( ! this.isConfigValid ) {
return;
}
- if ( ! this.contextHandler.validateContext() ) {
- return;
- }
-
- super.init();
-
this.allowedPaymentMethods = config.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
+ super.init();
this.initClient();
- this.initEventHandlers();
+
+ if ( ! this.isPresent ) {
+ this.log( 'Payment wrapper not found', this.wrapperId );
+ return;
+ }
this.paymentsClient
.isReadyToPay(
@@ -319,17 +132,7 @@ class GooglepayButton extends PaymentButton {
)
.then( ( response ) => {
this.log( 'PaymentsClient.isReadyToPay response:', response );
-
- /**
- * In case the button wrapper element is not present in the DOM yet, wait for it
- * to appear. Only proceed, if a button wrapper is found on this page.
- *
- * Not sure if this is needed, or if we can directly test for `this.isPresent`
- * without any delay.
- */
- this.waitForWrapper( () => {
- this.isEligible = !! response.result;
- } );
+ this.isEligible = !! response.result;
} )
.catch( ( err ) => {
console.error( err );
@@ -346,30 +149,6 @@ class GooglepayButton extends PaymentButton {
this.init();
}
- validateConfig() {
- if (
- [ 'PRODUCTION', 'TEST' ].indexOf(
- this.buttonConfig.environment
- ) === -1
- ) {
- console.error(
- '[GooglePayButton] Invalid environment.',
- this.buttonConfig.environment
- );
- return false;
- }
-
- if ( ! this.contextHandler ) {
- console.error(
- '[GooglePayButton] Invalid context handler.',
- this.contextHandler
- );
- return false;
- }
-
- return true;
- }
-
initClient() {
const callbacks = {
onPaymentAuthorized: this.onPaymentAuthorized.bind( this ),
@@ -394,51 +173,6 @@ class GooglepayButton extends PaymentButton {
} );
}
- initEventHandlers() {
- if ( CONTEXT.Gateways.includes( this.context ) ) {
- document.body.addEventListener( 'ppcp_invalidate_methods', () => {
- this.isVisible = false;
- } );
-
- document.body.addEventListener(
- `ppcp_render_method-${ PaymentMethods.GOOGLEPAY }`,
- () => {
- this.isVisible = true;
- }
- );
- } else {
- /**
- * Review: The following logic appears to be unnecessary. Is it still required?
- * /
- const ppcpButtonWrapper = `#${ this.ppcpButtonWrapperId }`;
- const wrapper = `#${ this.wrapperId }`;
- if ( wrapper === ppcpButtonWrapper ) {
- throw new Error(
- `[GooglePayButton] "wrapper" and "ppcpButtonWrapper" values must differ to avoid infinite loop. Current value: "${ wrapper }"`
- );
- }
- const syncButtonVisibility = () => {
- const $ppcpButtonWrapper = jQuery( ppcpButtonWrapper );
- setVisible( wrapper, $ppcpButtonWrapper.is( ':visible' ) );
- setEnabled(
- wrapper,
- ! $ppcpButtonWrapper.hasClass( 'ppcp-disabled' )
- );
- };
- jQuery( document ).on(
- 'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
- ( ev, data ) => {
- if ( jQuery( data.selector ).is( ppcpButtonWrapper ) ) {
- syncButtonVisibility();
- }
- }
- );
- syncButtonVisibility();
- //
- */
- }
- }
-
buildReadyToPayRequest( allowedPaymentMethods, baseRequest ) {
this.log( 'Ready To Pay request', baseRequest, allowedPaymentMethods );
@@ -448,25 +182,11 @@ class GooglepayButton extends PaymentButton {
}
/**
- * Add a Google Pay purchase button
+ * Add a Google Pay purchase button.
*/
addButton() {
- this.log( 'addButton' );
-
- const wrapper = this.wrapperElement;
const baseCardPaymentMethod = this.baseCardPaymentMethod;
- const { color, type, language } = this.buttonStyle;
- const { shape, height } = this.ppcpStyle;
-
- wrapper.classList.add(
- `ppcp-button-${ shape }`,
- 'ppcp-button-apm',
- 'ppcp-button-googlepay'
- );
-
- if ( height ) {
- wrapper.style.height = `${ height }px`;
- }
+ const { color, type, language } = this.style;
/**
* @see https://developers.google.com/pay/api/web/reference/client#createButton
@@ -480,84 +200,7 @@ class GooglepayButton extends PaymentButton {
buttonSizeMode: 'fill',
} );
- this.log( 'Insert Button', {
- wrapper,
- button,
- } );
-
- wrapper.replaceChildren( button );
- }
-
- /**
- * Waits for the current button's wrapper element to become available in the DOM.
- *
- * Not sure if still needed, or if a simple `this.isPresent` check is sufficient.
- *
- * @param {Function} callback Function to call when the wrapper element was detected. Only
- * called on success.
- * @param {number} delay Optional. Polling interval to inspect the DOM. Default to 0.1 sec
- * @param {number} timeout Optional. Max timeout in ms. Defaults to 2 sec
- */
- waitForWrapper( callback, delay = 100, timeout = 2000 ) {
- let interval = 0;
- const startTime = Date.now();
-
- const stop = () => {
- if ( interval ) {
- clearInterval( interval );
- }
- interval = 0;
- };
-
- const checkElement = () => {
- if ( this.isPresent ) {
- stop();
- callback();
- return;
- }
-
- const timeElapsed = Date.now() - startTime;
-
- if ( timeElapsed > timeout ) {
- stop();
- this.error( 'Wrapper not found:', this.wrapperId );
- }
- };
-
- interval = setInterval( checkElement, delay );
- }
-
- /**
- * Refreshes the payment button on the page.
- */
- refresh() {
- const showButtonWrapper = () => {
- this.log( 'Show' );
-
- // Classic Checkout: Make the Google Pay gateway visible.
- document
- .querySelectorAll( 'style#ppcp-hide-google-pay' )
- .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';
- } );
- };
-
- if ( this.isVisible && this.isEligible && this.isPresent ) {
- showButtonWrapper();
- this.addButton();
- } else {
- hideButtonWrapper();
- }
+ this.insertButton( button );
}
//------------------------
diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php
index 98bff2c97..2f87a487d 100644
--- a/modules/ppcp-googlepay/src/Assets/Button.php
+++ b/modules/ppcp-googlepay/src/Assets/Button.php
@@ -345,9 +345,12 @@ class Button implements ButtonInterface {
* @return void
*/
protected function hide_gateway_until_eligible() : void {
+
?>
-
Date: Wed, 7 Aug 2024 14:36:51 +0200
Subject: [PATCH 14/40] =?UTF-8?q?=F0=9F=A9=B9=20ConsoleLogger=20will=20alw?=
=?UTF-8?q?ays=20output=20error=20messages?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/GooglepayButton.js | 4 ++--
.../resources/js/helper/ConsoleLogger.js | 21 ++++++++++++++++---
2 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 832768401..ec0f4bd7a 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -227,8 +227,8 @@ class GooglepayButton extends PaymentButton {
this.paymentsClient.loadPaymentData( paymentDataRequest );
},
- () => {
- console.error( '[GooglePayButton] Form validation failed.' );
+ ( reason ) => {
+ this.error( 'Form validation failed.', reason );
}
);
}
diff --git a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
index 4b8891247..c76aa8960 100644
--- a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
+++ b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
@@ -24,10 +24,20 @@ export default class ConsoleLogger {
}
}
+ /**
+ * Enable or disable logging. Only impacts `log()` output.
+ *
+ * @param {boolean} state True to enable log output.
+ */
set enabled( state ) {
this.#enabled = state;
}
+ /**
+ * Output log-level details to the browser console, if logging is enabled.
+ *
+ * @param {...any} args - All provided values are output to the browser console.
+ */
log( ...args ) {
if ( this.#enabled ) {
// eslint-disable-next-line
@@ -35,9 +45,14 @@ export default class ConsoleLogger {
}
}
+ /**
+ * Generate an error message in the browser's console.
+ *
+ * Error messages are always output, even when logging is disabled.
+ *
+ * @param {...any} args - All provided values are output to the browser console.
+ */
error( ...args ) {
- if ( this.#enabled ) {
- console.error( this.#prefix, ...args );
- }
+ console.error( this.#prefix, ...args );
}
}
From 429568fbd9a1024399fe22d7369f0bb5726a4a83 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 7 Aug 2024 14:45:08 +0200
Subject: [PATCH 15/40] =?UTF-8?q?=F0=9F=94=A5=20Minor=20cleanup?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Renderer/PaymentButton.js | 4 ----
modules/ppcp-googlepay/resources/js/GooglepayButton.js | 2 --
modules/ppcp-googlepay/src/Assets/Button.php | 3 +--
3 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
index 8275d1ee4..9e1def508 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -503,9 +503,5 @@ export default class PaymentButton {
this.#button.remove();
}
this.#button = null;
-
- const wrapper = this.wrapperElement;
-
- wrapper.innerHTML = '';
}
}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index ec0f4bd7a..af4d977d5 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -50,8 +50,6 @@ class GooglepayButton extends PaymentButton {
'ppc-button-ppcp-googlepay'
);
- console.log( ppcpConfig.button, buttonConfig.button );
-
const styles = combineStyles( ppcpConfig.button, buttonConfig.button );
if ( 'buy' === styles.MiniCart.type ) {
diff --git a/modules/ppcp-googlepay/src/Assets/Button.php b/modules/ppcp-googlepay/src/Assets/Button.php
index 2f87a487d..575def21f 100644
--- a/modules/ppcp-googlepay/src/Assets/Button.php
+++ b/modules/ppcp-googlepay/src/Assets/Button.php
@@ -339,13 +339,12 @@ class Button implements ButtonInterface {
/**
* Outputs an inline CSS style that hides the Google Pay gateway (on Classic Checkout).
- * The style is removed by `GooglepayButton.js` once the eligibility of the payment method
+ * The style is removed by `PaymentButton.js` once the eligibility of the payment method
* is confirmed.
*
* @return void
*/
protected function hide_gateway_until_eligible() : void {
-
?>