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 { - ?>