diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js index e3df4c969..aa53b9488 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js @@ -170,6 +170,11 @@ export default class PaymentButton { */ #contextHandler; + /** + * Button attributes. + */ + #buttonAttributes; + /** * Whether the current browser/website support the payment method. * @@ -195,11 +200,12 @@ export default class PaymentButton { /** * Factory method to create a new PaymentButton while limiting a single instance per context. * - * @param {string} context - Button context name. - * @param {unknown} externalHandler - Handler object. - * @param {Object} buttonConfig - Payment button specific configuration. - * @param {Object} ppcpConfig - Plugin wide configuration object. - * @param {unknown} contextHandler - Handler object. + * @param {string} context - Button context name. + * @param {unknown} externalHandler - Handler object. + * @param {Object} buttonConfig - Payment button specific configuration. + * @param {Object} ppcpConfig - Plugin wide configuration object. + * @param {unknown} contextHandler - Handler object. + * @param {Object} buttonAttributes - Button attributes. * @return {PaymentButton} The button instance. */ static createButton( @@ -207,7 +213,8 @@ export default class PaymentButton { externalHandler, buttonConfig, ppcpConfig, - contextHandler + contextHandler, + buttonAttributes ) { const buttonInstances = getInstances(); const instanceKey = `${ this.methodId }.${ context }`; @@ -218,7 +225,8 @@ export default class PaymentButton { externalHandler, buttonConfig, ppcpConfig, - contextHandler + contextHandler, + buttonAttributes ); buttonInstances.set( instanceKey, button ); @@ -262,18 +270,20 @@ export default class PaymentButton { * to avoid multiple button instances handling the same context. * * @private - * @param {string} context - Button context name. - * @param {Object} externalHandler - Handler object. - * @param {Object} buttonConfig - Payment button specific configuration. - * @param {Object} ppcpConfig - Plugin wide configuration object. - * @param {Object} contextHandler - Handler object. + * @param {string} context - Button context name. + * @param {Object} externalHandler - Handler object. + * @param {Object} buttonConfig - Payment button specific configuration. + * @param {Object} ppcpConfig - Plugin wide configuration object. + * @param {Object} contextHandler - Handler object. + * @param {Object} buttonAttributes - Button attributes. */ constructor( context, externalHandler = null, buttonConfig = {}, ppcpConfig = {}, - contextHandler = null + contextHandler = null, + buttonAttributes = {} ) { if ( this.methodId === PaymentButton.methodId ) { throw new Error( 'Cannot initialize the PaymentButton base class' ); @@ -291,6 +301,7 @@ export default class PaymentButton { this.#ppcpConfig = ppcpConfig; this.#externalHandler = externalHandler; this.#contextHandler = contextHandler; + this.#buttonAttributes = buttonAttributes; this.#logger = new ConsoleLogger( methodName, context ); @@ -763,15 +774,20 @@ export default class PaymentButton { const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`; const wrapperSelector = `#${ this.wrappers.Default }`; - const paymentMethodLi = document.querySelector(`.wc_payment_method.payment_method_${ this.methodId }`); + const paymentMethodLi = document.querySelector( + `.wc_payment_method.payment_method_${ this.methodId }` + ); document .querySelectorAll( styleSelector ) .forEach( ( el ) => el.remove() ); - if (paymentMethodLi.style.display === 'none' || paymentMethodLi.style.display === '') { - paymentMethodLi.style.display = 'block'; - } + if ( + paymentMethodLi.style.display === 'none' || + paymentMethodLi.style.display === '' + ) { + paymentMethodLi.style.display = 'block'; + } document .querySelectorAll( wrapperSelector ) @@ -843,7 +859,7 @@ export default class PaymentButton { this.removeButton(); } - this.log( 'addButton', button ); + this.log( 'insertButton', button ); this.#button = button; wrapper.appendChild( this.#button ); diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss index 3c6fe8912..6fb0015e9 100644 --- a/modules/ppcp-googlepay/resources/css/styles.scss +++ b/modules/ppcp-googlepay/resources/css/styles.scss @@ -1,7 +1,6 @@ /* Front end display */ .ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container { outline-offset: -1px; - border-radius: var(--apm-button-border-radius); } /* Admin preview */ diff --git a/modules/ppcp-googlepay/resources/js/Block/components/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/Block/components/GooglepayButton.js index a5caab59d..866b0a7a4 100644 --- a/modules/ppcp-googlepay/resources/js/Block/components/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/Block/components/GooglepayButton.js @@ -1,12 +1,15 @@ -import { useState, useEffect } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import useGooglepayApiToGenerateButton from '../hooks/useGooglepayApiToGenerateButton'; import usePayPalScript from '../hooks/usePayPalScript'; import useGooglepayScript from '../hooks/useGooglepayScript'; import useGooglepayConfig from '../hooks/useGooglepayConfig'; -const GooglepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => { - const [ buttonHtml, setButtonHtml ] = useState( '' ); - const [ buttonElement, setButtonElement ] = useState( null ); +const GooglepayButton = ( { + namespace, + buttonConfig, + ppcpConfig, + buttonAttributes, +} ) => { const [ componentFrame, setComponentFrame ] = useState( null ); const isPayPalLoaded = usePayPalScript( namespace, ppcpConfig ); @@ -18,35 +21,45 @@ const GooglepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => { const googlepayConfig = useGooglepayConfig( namespace, isGooglepayLoaded ); - useEffect( () => { - if ( ! buttonElement ) { - return; - } - - setComponentFrame( buttonElement.ownerDocument ); - }, [ buttonElement ] ); - - const googlepayButton = useGooglepayApiToGenerateButton( + const { button, containerStyles } = useGooglepayApiToGenerateButton( componentFrame, namespace, buttonConfig, ppcpConfig, - googlepayConfig + googlepayConfig, + buttonAttributes ); - useEffect( () => { - if ( googlepayButton ) { - const hideLoader = - ''; - setButtonHtml( googlepayButton.outerHTML + hideLoader ); - } - }, [ googlepayButton ] ); - return ( -
+ <> +
{ + if ( ! node ) { + return; + } + + // Set component frame + setComponentFrame( node.ownerDocument ); + + // Handle button mounting + while ( node.firstChild ) { + node.removeChild( node.firstChild ); + } + if ( button ) { + node.appendChild( button ); + } + } } + /> + { button && ( + + ) } + ); }; diff --git a/modules/ppcp-googlepay/resources/js/Block/hooks/useButtonStyles.js b/modules/ppcp-googlepay/resources/js/Block/hooks/useButtonStyles.js index 6e214acb2..c4dc71f92 100644 --- a/modules/ppcp-googlepay/resources/js/Block/hooks/useButtonStyles.js +++ b/modules/ppcp-googlepay/resources/js/Block/hooks/useButtonStyles.js @@ -1,19 +1,26 @@ import { useMemo } from '@wordpress/element'; import { combineStyles } from '../../../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers'; -const useButtonStyles = ( buttonConfig, ppcpConfig ) => { +const useButtonStyles = ( buttonConfig, ppcpConfig, buttonAttributes ) => { return useMemo( () => { const styles = combineStyles( ppcpConfig?.button || {}, buttonConfig?.button || {} ); - if ( styles.MiniCart && styles.MiniCart.type === 'buy' ) { + if ( buttonAttributes && styles.Default ) { + styles.Default.height = + buttonAttributes.height || styles.Default.height; + styles.Default.borderRadius = + buttonAttributes.borderRadius || styles.Default.borderRadius; + } + + if ( styles.MiniCart?.type === 'buy' ) { styles.MiniCart.type = 'pay'; } return styles; - }, [ buttonConfig, ppcpConfig ] ); + }, [ buttonConfig, ppcpConfig, buttonAttributes ] ); }; export default useButtonStyles; diff --git a/modules/ppcp-googlepay/resources/js/Block/hooks/useGooglepayApiToGenerateButton.js b/modules/ppcp-googlepay/resources/js/Block/hooks/useGooglepayApiToGenerateButton.js index fe5b342a7..295b8c656 100644 --- a/modules/ppcp-googlepay/resources/js/Block/hooks/useGooglepayApiToGenerateButton.js +++ b/modules/ppcp-googlepay/resources/js/Block/hooks/useGooglepayApiToGenerateButton.js @@ -6,10 +6,16 @@ const useGooglepayApiToGenerateButton = ( namespace, buttonConfig, ppcpConfig, - googlepayConfig + googlepayConfig, + buttonAttributes ) => { const [ googlepayButton, setGooglepayButton ] = useState( null ); - const buttonStyles = useButtonStyles( buttonConfig, ppcpConfig ); + + const buttonStyles = useButtonStyles( + buttonConfig, + ppcpConfig, + buttonAttributes + ); useEffect( () => { if ( @@ -35,14 +41,13 @@ const useGooglepayApiToGenerateButton = ( buttonType: buttonConfig.buttonType || 'pay', buttonLocale: buttonConfig.buttonLocale || 'en', buttonSizeMode: 'fill', - }; - - const button = paymentsClient.createButton( { - ...googlePayButtonOptions, + buttonRadius: parseInt( buttonStyles?.Default?.borderRadius ), onClick: ( event ) => { event.preventDefault(); }, - } ); + }; + + const button = paymentsClient.createButton( googlePayButtonOptions ); setGooglepayButton( button ); @@ -51,7 +56,15 @@ const useGooglepayApiToGenerateButton = ( }; }, [ namespace, buttonConfig, ppcpConfig, googlepayConfig, buttonStyles ] ); - return googlepayButton; + // Return both the button and the styles needed for the container + return { + button: googlepayButton, + containerStyles: { + height: buttonStyles?.Default?.height + ? `${ buttonStyles.Default.height }px` + : '', + }, + }; }; export default useGooglepayApiToGenerateButton; diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js index 97bfa6d2c..2b8fb044d 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js @@ -106,8 +106,40 @@ class GooglepayButton extends PaymentButton { */ #transactionInfo = null; + /** + * The currently visible payment button. + * + * @type {HTMLElement|null} + */ + #button = null; + + /** + * The Google Pay configuration object. + * + * @type {Object|null} + */ googlePayConfig = null; + /** + * The start time of the configuration process. + * + * @type {number} + */ + #configureStartTime = 0; + + /** + * The maximum time to wait for buttonAttributes before proceeding with initialization. + * @type {number} + */ + #maxWaitTime = 1000; + + /** + * The stored button attributes. + * + * @type {null} + */ + #storedButtonAttributes = null; + /** * @inheritDoc */ @@ -142,7 +174,8 @@ class GooglepayButton extends PaymentButton { externalHandler, buttonConfig, ppcpConfig, - contextHandler + contextHandler, + buttonAttributes ) { // Disable debug output in the browser console: // buttonConfig.is_debug = false; @@ -152,7 +185,8 @@ class GooglepayButton extends PaymentButton { externalHandler, buttonConfig, ppcpConfig, - contextHandler + contextHandler, + buttonAttributes ); this.init = this.init.bind( this ); @@ -249,6 +283,22 @@ class GooglepayButton extends PaymentButton { ); } + // Add buttonAttributes validation + if ( this.buttonAttributes ) { + if ( + this.buttonAttributes.height && + isNaN( parseInt( this.buttonAttributes.height ) ) + ) { + return isInvalid( 'Invalid height in buttonAttributes' ); + } + if ( + this.buttonAttributes.borderRadius && + isNaN( parseInt( this.buttonAttributes.borderRadius ) ) + ) { + return isInvalid( 'Invalid borderRadius in buttonAttributes' ); + } + } + if ( ! typeof this.contextHandler?.validateContext() ) { return isInvalid( 'Invalid context handler.', this.contextHandler ); } @@ -259,24 +309,74 @@ class GooglepayButton extends PaymentButton { /** * Configures the button instance. Must be called before the initial `init()`. * - * @param {Object} apiConfig - API configuration. - * @param {Object} transactionInfo - Transaction details; required before "init" call. + * @param {Object} apiConfig - API configuration. + * @param {Object} transactionInfo - Transaction details; required before "init" call. + * @param {Object} buttonAttributes - Button attributes. */ - configure( apiConfig, transactionInfo ) { + configure( apiConfig, transactionInfo, buttonAttributes = {} ) { + // Start timing on first configure call + if ( ! this.#configureStartTime ) { + this.#configureStartTime = Date.now(); + } + + // If valid buttonAttributes, store them + if ( buttonAttributes?.height && buttonAttributes?.borderRadius ) { + this.#storedButtonAttributes = { ...buttonAttributes }; + } + + // Use stored attributes if current ones are missing + const attributes = buttonAttributes?.height + ? buttonAttributes + : this.#storedButtonAttributes; + + // Check if we've exceeded wait time + const timeWaited = Date.now() - this.#configureStartTime; + if ( timeWaited > this.#maxWaitTime ) { + this.log( + 'GooglePay: Timeout waiting for buttonAttributes - proceeding with initialization' + ); + this.googlePayConfig = apiConfig; + this.#transactionInfo = transactionInfo; + this.buttonAttributes = attributes || buttonAttributes; + this.allowedPaymentMethods = + this.googlePayConfig.allowedPaymentMethods; + this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ]; + this.init(); + return; + } + + // Block any initialization until we have valid buttonAttributes + if ( ! attributes?.height || ! attributes?.borderRadius ) { + setTimeout( + () => + this.configure( + apiConfig, + transactionInfo, + buttonAttributes + ), + 100 + ); + return; + } + + // Reset timer for future configure calls + this.#configureStartTime = 0; + this.googlePayConfig = apiConfig; this.#transactionInfo = transactionInfo; - + this.buttonAttributes = attributes; this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods; this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ]; + this.init(); } init() { - // Use `reinit()` to force a full refresh of an initialized button. + // Skip if already initialized if ( this.isInitialized ) { return; } - // Stop, if configuration is invalid. + // Validate configuration if ( ! this.validateConfiguration() ) { return; } @@ -284,16 +384,6 @@ class GooglepayButton extends PaymentButton { super.init(); this.#paymentsClient = this.createPaymentsClient(); - if ( ! this.isPresent ) { - this.log( 'Payment wrapper not found', this.wrapperId ); - return; - } - - if ( ! this.paymentsClient ) { - this.log( 'Could not initialize the payments client' ); - return; - } - this.paymentsClient .isReadyToPay( this.buildReadyToPayRequest( @@ -371,30 +461,86 @@ class GooglepayButton extends PaymentButton { } /** - * Creates the payment button and calls `this.insertButton()` to make the button visible in the - * correct wrapper. + * Creates the payment button and calls `super.insertButton()` to make the button visible in the correct wrapper. */ addButton() { if ( ! this.paymentsClient ) { return; } + // If current buttonAttributes are missing, try to use stored ones + if ( + ! this.buttonAttributes?.height && + this.#storedButtonAttributes?.height + ) { + this.buttonAttributes = { ...this.#storedButtonAttributes }; + } + + this.removeButton(); + const baseCardPaymentMethod = this.baseCardPaymentMethod; const { color, type, language } = this.style; - /** - * @see https://developers.google.com/pay/api/web/reference/client#createButton - */ - const button = this.paymentsClient.createButton( { + const buttonOptions = { + buttonColor: color || 'black', + buttonSizeMode: 'fill', + buttonLocale: language || 'en', + buttonType: type || 'pay', + buttonRadius: parseInt( this.buttonAttributes?.borderRadius, 10 ), onClick: this.onButtonClick, allowedPaymentMethods: [ baseCardPaymentMethod ], - buttonColor: color || 'black', - buttonType: type || 'pay', - buttonLocale: language || 'en', - buttonSizeMode: 'fill', - } ); + }; - this.insertButton( button ); + const button = this.paymentsClient.createButton( buttonOptions ); + this.#button = button; + + super.insertButton( button ); + this.applyWrapperStyles(); + } + + /** + * Applies CSS classes and inline styling to the payment button wrapper. + * Extends parent implementation to handle Google Pay specific styling. + */ + applyWrapperStyles() { + super.applyWrapperStyles(); + + const wrapper = this.wrapperElement; + if ( ! wrapper ) { + return; + } + + // Try stored attributes if current ones are missing + const attributes = this.buttonAttributes?.height + ? this.buttonAttributes + : this.#storedButtonAttributes; + + if ( attributes?.height ) { + const height = parseInt( attributes.height, 10 ); + if ( ! isNaN( height ) ) { + wrapper.style.height = `${ height }px`; + wrapper.style.minHeight = `${ height }px`; + } + } + } + + /** + * Removes the payment button from the DOM. + */ + removeButton() { + if ( ! this.isPresent || ! this.#button ) { + return; + } + + this.log( 'removeButton' ); + + try { + this.wrapperElement.removeChild( this.#button ); + } catch ( Exception ) { + // Ignore this. + } + + this.#button = null; } //------------------------ diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManager.js b/modules/ppcp-googlepay/resources/js/GooglepayManager.js index f9520d23a..8db6c762c 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManager.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManager.js @@ -3,10 +3,11 @@ import GooglepayButton from './GooglepayButton'; import ContextHandlerFactory from './Context/ContextHandlerFactory'; class GooglepayManager { - constructor( namespace, buttonConfig, ppcpConfig ) { + constructor( namespace, buttonConfig, ppcpConfig, buttonAttributes = {} ) { this.namespace = namespace; this.buttonConfig = buttonConfig; this.ppcpConfig = ppcpConfig; + this.buttonAttributes = buttonAttributes; this.googlePayConfig = null; this.transactionInfo = null; this.contextHandler = null; @@ -26,13 +27,18 @@ class GooglepayManager { bootstrap.handler, buttonConfig, ppcpConfig, - this.contextHandler + this.contextHandler, + this.buttonAttributes ); this.buttons.push( button ); const initButton = () => { - button.configure( this.googlePayConfig, this.transactionInfo ); + button.configure( + this.googlePayConfig, + this.transactionInfo, + this.buttonAttributes + ); button.init(); }; @@ -70,7 +76,8 @@ class GooglepayManager { for ( const button of this.buttons ) { button.configure( this.googlePayConfig, - this.transactionInfo + this.transactionInfo, + this.buttonAttributes ); button.init(); } diff --git a/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js b/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js index 1e3bd6ebb..548e37f8b 100644 --- a/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js +++ b/modules/ppcp-googlepay/resources/js/GooglepayManagerBlockEditor.js @@ -4,11 +4,13 @@ const GooglepayManagerBlockEditor = ( { namespace, buttonConfig, ppcpConfig, + buttonAttributes, } ) => ( ); diff --git a/modules/ppcp-googlepay/resources/js/boot-block.js b/modules/ppcp-googlepay/resources/js/boot-block.js index 398cc488c..8c63bba14 100644 --- a/modules/ppcp-googlepay/resources/js/boot-block.js +++ b/modules/ppcp-googlepay/resources/js/boot-block.js @@ -20,7 +20,7 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) { window.PayPalCommerceGateway = ppcpConfig; } -const GooglePayComponent = ( { isEditing } ) => { +const GooglePayComponent = ( { isEditing, buttonAttributes } ) => { const [ paypalLoaded, setPaypalLoaded ] = useState( false ); const [ googlePayLoaded, setGooglePayLoaded ] = useState( false ); const [ manager, setManager ] = useState( null ); @@ -48,11 +48,18 @@ const GooglePayComponent = ( { isEditing } ) => { const newManager = new GooglepayManager( namespace, buttonConfig, - ppcpConfig + ppcpConfig, + buttonAttributes ); setManager( newManager ); } - }, [ paypalLoaded, googlePayLoaded, isEditing, manager ] ); + }, [ + paypalLoaded, + googlePayLoaded, + isEditing, + manager, + buttonAttributes, + ] ); if ( isEditing ) { return ( @@ -60,6 +67,7 @@ const GooglePayComponent = ( { isEditing } ) => { namespace={ namespace } buttonConfig={ buttonConfig } ppcpConfig={ ppcpConfig } + buttonAttributes={ buttonAttributes } /> ); } @@ -89,5 +97,6 @@ registerExpressPaymentMethod( { canMakePayment: () => buttonData.enabled, supports: { features, + style: [ 'height', 'borderRadius' ], }, } );