Add ButtonOptions support for the Google Pay button

This commit is contained in:
Daniel Dudzic 2024-11-07 01:00:53 +01:00
parent 7832f853ff
commit b117ff9b7c
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
9 changed files with 305 additions and 93 deletions

View file

@ -170,6 +170,11 @@ export default class PaymentButton {
*/ */
#contextHandler; #contextHandler;
/**
* Button attributes.
*/
#buttonAttributes;
/** /**
* Whether the current browser/website support the payment method. * 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. * Factory method to create a new PaymentButton while limiting a single instance per context.
* *
* @param {string} context - Button context name. * @param {string} context - Button context name.
* @param {unknown} externalHandler - Handler object. * @param {unknown} externalHandler - Handler object.
* @param {Object} buttonConfig - Payment button specific configuration. * @param {Object} buttonConfig - Payment button specific configuration.
* @param {Object} ppcpConfig - Plugin wide configuration object. * @param {Object} ppcpConfig - Plugin wide configuration object.
* @param {unknown} contextHandler - Handler object. * @param {unknown} contextHandler - Handler object.
* @param {Object} buttonAttributes - Button attributes.
* @return {PaymentButton} The button instance. * @return {PaymentButton} The button instance.
*/ */
static createButton( static createButton(
@ -207,7 +213,8 @@ export default class PaymentButton {
externalHandler, externalHandler,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
contextHandler contextHandler,
buttonAttributes
) { ) {
const buttonInstances = getInstances(); const buttonInstances = getInstances();
const instanceKey = `${ this.methodId }.${ context }`; const instanceKey = `${ this.methodId }.${ context }`;
@ -218,7 +225,8 @@ export default class PaymentButton {
externalHandler, externalHandler,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
contextHandler contextHandler,
buttonAttributes
); );
buttonInstances.set( instanceKey, button ); buttonInstances.set( instanceKey, button );
@ -262,18 +270,20 @@ export default class PaymentButton {
* to avoid multiple button instances handling the same context. * to avoid multiple button instances handling the same context.
* *
* @private * @private
* @param {string} context - Button context name. * @param {string} context - Button context name.
* @param {Object} externalHandler - Handler object. * @param {Object} externalHandler - Handler object.
* @param {Object} buttonConfig - Payment button specific configuration. * @param {Object} buttonConfig - Payment button specific configuration.
* @param {Object} ppcpConfig - Plugin wide configuration object. * @param {Object} ppcpConfig - Plugin wide configuration object.
* @param {Object} contextHandler - Handler object. * @param {Object} contextHandler - Handler object.
* @param {Object} buttonAttributes - Button attributes.
*/ */
constructor( constructor(
context, context,
externalHandler = null, externalHandler = null,
buttonConfig = {}, buttonConfig = {},
ppcpConfig = {}, ppcpConfig = {},
contextHandler = null contextHandler = null,
buttonAttributes = {}
) { ) {
if ( this.methodId === PaymentButton.methodId ) { if ( this.methodId === PaymentButton.methodId ) {
throw new Error( 'Cannot initialize the PaymentButton base class' ); throw new Error( 'Cannot initialize the PaymentButton base class' );
@ -291,6 +301,7 @@ export default class PaymentButton {
this.#ppcpConfig = ppcpConfig; this.#ppcpConfig = ppcpConfig;
this.#externalHandler = externalHandler; this.#externalHandler = externalHandler;
this.#contextHandler = contextHandler; this.#contextHandler = contextHandler;
this.#buttonAttributes = buttonAttributes;
this.#logger = new ConsoleLogger( methodName, context ); this.#logger = new ConsoleLogger( methodName, context );
@ -763,15 +774,20 @@ export default class PaymentButton {
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`; const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
const wrapperSelector = `#${ this.wrappers.Default }`; 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 document
.querySelectorAll( styleSelector ) .querySelectorAll( styleSelector )
.forEach( ( el ) => el.remove() ); .forEach( ( el ) => el.remove() );
if (paymentMethodLi.style.display === 'none' || paymentMethodLi.style.display === '') { if (
paymentMethodLi.style.display = 'block'; paymentMethodLi.style.display === 'none' ||
} paymentMethodLi.style.display === ''
) {
paymentMethodLi.style.display = 'block';
}
document document
.querySelectorAll( wrapperSelector ) .querySelectorAll( wrapperSelector )
@ -843,7 +859,7 @@ export default class PaymentButton {
this.removeButton(); this.removeButton();
} }
this.log( 'addButton', button ); this.log( 'insertButton', button );
this.#button = button; this.#button = button;
wrapper.appendChild( this.#button ); wrapper.appendChild( this.#button );

View file

@ -1,7 +1,6 @@
/* Front end display */ /* Front end display */
.ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container { .ppcp-button-apm .gpay-card-info-container-fill .gpay-card-info-container {
outline-offset: -1px; outline-offset: -1px;
border-radius: var(--apm-button-border-radius);
} }
/* Admin preview */ /* Admin preview */

View file

@ -1,12 +1,15 @@
import { useState, useEffect } from '@wordpress/element'; import { useState } from '@wordpress/element';
import useGooglepayApiToGenerateButton from '../hooks/useGooglepayApiToGenerateButton'; import useGooglepayApiToGenerateButton from '../hooks/useGooglepayApiToGenerateButton';
import usePayPalScript from '../hooks/usePayPalScript'; import usePayPalScript from '../hooks/usePayPalScript';
import useGooglepayScript from '../hooks/useGooglepayScript'; import useGooglepayScript from '../hooks/useGooglepayScript';
import useGooglepayConfig from '../hooks/useGooglepayConfig'; import useGooglepayConfig from '../hooks/useGooglepayConfig';
const GooglepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => { const GooglepayButton = ( {
const [ buttonHtml, setButtonHtml ] = useState( '' ); namespace,
const [ buttonElement, setButtonElement ] = useState( null ); buttonConfig,
ppcpConfig,
buttonAttributes,
} ) => {
const [ componentFrame, setComponentFrame ] = useState( null ); const [ componentFrame, setComponentFrame ] = useState( null );
const isPayPalLoaded = usePayPalScript( namespace, ppcpConfig ); const isPayPalLoaded = usePayPalScript( namespace, ppcpConfig );
@ -18,35 +21,45 @@ const GooglepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => {
const googlepayConfig = useGooglepayConfig( namespace, isGooglepayLoaded ); const googlepayConfig = useGooglepayConfig( namespace, isGooglepayLoaded );
useEffect( () => { const { button, containerStyles } = useGooglepayApiToGenerateButton(
if ( ! buttonElement ) {
return;
}
setComponentFrame( buttonElement.ownerDocument );
}, [ buttonElement ] );
const googlepayButton = useGooglepayApiToGenerateButton(
componentFrame, componentFrame,
namespace, namespace,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
googlepayConfig googlepayConfig,
buttonAttributes
); );
useEffect( () => {
if ( googlepayButton ) {
const hideLoader =
'<style>.block-editor-iframe__html .gpay-card-info-animated-progress-bar-container {display:none !important}</style>';
setButtonHtml( googlepayButton.outerHTML + hideLoader );
}
}, [ googlepayButton ] );
return ( return (
<div <>
ref={ setButtonElement } <div
dangerouslySetInnerHTML={ { __html: buttonHtml } } id="express-payment-method-ppcp-googlepay"
/> style={ containerStyles }
ref={ ( node ) => {
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 && (
<style>
{ `.block-editor-iframe__html .gpay-card-info-animated-progress-bar-container {
display: none !important
}` }
</style>
) }
</>
); );
}; };

View file

@ -1,19 +1,26 @@
import { useMemo } from '@wordpress/element'; import { useMemo } from '@wordpress/element';
import { combineStyles } from '../../../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers'; import { combineStyles } from '../../../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
const useButtonStyles = ( buttonConfig, ppcpConfig ) => { const useButtonStyles = ( buttonConfig, ppcpConfig, buttonAttributes ) => {
return useMemo( () => { return useMemo( () => {
const styles = combineStyles( const styles = combineStyles(
ppcpConfig?.button || {}, ppcpConfig?.button || {},
buttonConfig?.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'; styles.MiniCart.type = 'pay';
} }
return styles; return styles;
}, [ buttonConfig, ppcpConfig ] ); }, [ buttonConfig, ppcpConfig, buttonAttributes ] );
}; };
export default useButtonStyles; export default useButtonStyles;

View file

@ -6,10 +6,16 @@ const useGooglepayApiToGenerateButton = (
namespace, namespace,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
googlepayConfig googlepayConfig,
buttonAttributes
) => { ) => {
const [ googlepayButton, setGooglepayButton ] = useState( null ); const [ googlepayButton, setGooglepayButton ] = useState( null );
const buttonStyles = useButtonStyles( buttonConfig, ppcpConfig );
const buttonStyles = useButtonStyles(
buttonConfig,
ppcpConfig,
buttonAttributes
);
useEffect( () => { useEffect( () => {
if ( if (
@ -35,14 +41,13 @@ const useGooglepayApiToGenerateButton = (
buttonType: buttonConfig.buttonType || 'pay', buttonType: buttonConfig.buttonType || 'pay',
buttonLocale: buttonConfig.buttonLocale || 'en', buttonLocale: buttonConfig.buttonLocale || 'en',
buttonSizeMode: 'fill', buttonSizeMode: 'fill',
}; buttonRadius: parseInt( buttonStyles?.Default?.borderRadius ),
const button = paymentsClient.createButton( {
...googlePayButtonOptions,
onClick: ( event ) => { onClick: ( event ) => {
event.preventDefault(); event.preventDefault();
}, },
} ); };
const button = paymentsClient.createButton( googlePayButtonOptions );
setGooglepayButton( button ); setGooglepayButton( button );
@ -51,7 +56,15 @@ const useGooglepayApiToGenerateButton = (
}; };
}, [ namespace, buttonConfig, ppcpConfig, googlepayConfig, buttonStyles ] ); }, [ 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; export default useGooglepayApiToGenerateButton;

View file

@ -106,8 +106,40 @@ class GooglepayButton extends PaymentButton {
*/ */
#transactionInfo = null; #transactionInfo = null;
/**
* The currently visible payment button.
*
* @type {HTMLElement|null}
*/
#button = null;
/**
* The Google Pay configuration object.
*
* @type {Object|null}
*/
googlePayConfig = 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 * @inheritDoc
*/ */
@ -142,7 +174,8 @@ class GooglepayButton extends PaymentButton {
externalHandler, externalHandler,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
contextHandler contextHandler,
buttonAttributes
) { ) {
// Disable debug output in the browser console: // Disable debug output in the browser console:
// buttonConfig.is_debug = false; // buttonConfig.is_debug = false;
@ -152,7 +185,8 @@ class GooglepayButton extends PaymentButton {
externalHandler, externalHandler,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
contextHandler contextHandler,
buttonAttributes
); );
this.init = this.init.bind( this ); 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() ) { if ( ! typeof this.contextHandler?.validateContext() ) {
return isInvalid( 'Invalid context handler.', this.contextHandler ); 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()`. * Configures the button instance. Must be called before the initial `init()`.
* *
* @param {Object} apiConfig - API configuration. * @param {Object} apiConfig - API configuration.
* @param {Object} transactionInfo - Transaction details; required before "init" call. * @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.googlePayConfig = apiConfig;
this.#transactionInfo = transactionInfo; this.#transactionInfo = transactionInfo;
this.buttonAttributes = attributes;
this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods; this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods;
this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ]; this.baseCardPaymentMethod = this.allowedPaymentMethods[ 0 ];
this.init();
} }
init() { init() {
// Use `reinit()` to force a full refresh of an initialized button. // Skip if already initialized
if ( this.isInitialized ) { if ( this.isInitialized ) {
return; return;
} }
// Stop, if configuration is invalid. // Validate configuration
if ( ! this.validateConfiguration() ) { if ( ! this.validateConfiguration() ) {
return; return;
} }
@ -284,16 +384,6 @@ class GooglepayButton extends PaymentButton {
super.init(); super.init();
this.#paymentsClient = this.createPaymentsClient(); 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 this.paymentsClient
.isReadyToPay( .isReadyToPay(
this.buildReadyToPayRequest( 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 * Creates the payment button and calls `super.insertButton()` to make the button visible in the correct wrapper.
* correct wrapper.
*/ */
addButton() { addButton() {
if ( ! this.paymentsClient ) { if ( ! this.paymentsClient ) {
return; 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 baseCardPaymentMethod = this.baseCardPaymentMethod;
const { color, type, language } = this.style; const { color, type, language } = this.style;
/** const buttonOptions = {
* @see https://developers.google.com/pay/api/web/reference/client#createButton buttonColor: color || 'black',
*/ buttonSizeMode: 'fill',
const button = this.paymentsClient.createButton( { buttonLocale: language || 'en',
buttonType: type || 'pay',
buttonRadius: parseInt( this.buttonAttributes?.borderRadius, 10 ),
onClick: this.onButtonClick, onClick: this.onButtonClick,
allowedPaymentMethods: [ baseCardPaymentMethod ], 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;
} }
//------------------------ //------------------------

View file

@ -3,10 +3,11 @@ import GooglepayButton from './GooglepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory'; import ContextHandlerFactory from './Context/ContextHandlerFactory';
class GooglepayManager { class GooglepayManager {
constructor( namespace, buttonConfig, ppcpConfig ) { constructor( namespace, buttonConfig, ppcpConfig, buttonAttributes = {} ) {
this.namespace = namespace; this.namespace = namespace;
this.buttonConfig = buttonConfig; this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig; this.ppcpConfig = ppcpConfig;
this.buttonAttributes = buttonAttributes;
this.googlePayConfig = null; this.googlePayConfig = null;
this.transactionInfo = null; this.transactionInfo = null;
this.contextHandler = null; this.contextHandler = null;
@ -26,13 +27,18 @@ class GooglepayManager {
bootstrap.handler, bootstrap.handler,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
this.contextHandler this.contextHandler,
this.buttonAttributes
); );
this.buttons.push( button ); this.buttons.push( button );
const initButton = () => { const initButton = () => {
button.configure( this.googlePayConfig, this.transactionInfo ); button.configure(
this.googlePayConfig,
this.transactionInfo,
this.buttonAttributes
);
button.init(); button.init();
}; };
@ -70,7 +76,8 @@ class GooglepayManager {
for ( const button of this.buttons ) { for ( const button of this.buttons ) {
button.configure( button.configure(
this.googlePayConfig, this.googlePayConfig,
this.transactionInfo this.transactionInfo,
this.buttonAttributes
); );
button.init(); button.init();
} }

View file

@ -4,11 +4,13 @@ const GooglepayManagerBlockEditor = ( {
namespace, namespace,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
buttonAttributes,
} ) => ( } ) => (
<GooglepayButton <GooglepayButton
namespace={ namespace } namespace={ namespace }
buttonConfig={ buttonConfig } buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig } ppcpConfig={ ppcpConfig }
buttonAttributes={ buttonAttributes }
/> />
); );

View file

@ -20,7 +20,7 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig; window.PayPalCommerceGateway = ppcpConfig;
} }
const GooglePayComponent = ( { isEditing } ) => { const GooglePayComponent = ( { isEditing, buttonAttributes } ) => {
const [ paypalLoaded, setPaypalLoaded ] = useState( false ); const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ googlePayLoaded, setGooglePayLoaded ] = useState( false ); const [ googlePayLoaded, setGooglePayLoaded ] = useState( false );
const [ manager, setManager ] = useState( null ); const [ manager, setManager ] = useState( null );
@ -48,11 +48,18 @@ const GooglePayComponent = ( { isEditing } ) => {
const newManager = new GooglepayManager( const newManager = new GooglepayManager(
namespace, namespace,
buttonConfig, buttonConfig,
ppcpConfig ppcpConfig,
buttonAttributes
); );
setManager( newManager ); setManager( newManager );
} }
}, [ paypalLoaded, googlePayLoaded, isEditing, manager ] ); }, [
paypalLoaded,
googlePayLoaded,
isEditing,
manager,
buttonAttributes,
] );
if ( isEditing ) { if ( isEditing ) {
return ( return (
@ -60,6 +67,7 @@ const GooglePayComponent = ( { isEditing } ) => {
namespace={ namespace } namespace={ namespace }
buttonConfig={ buttonConfig } buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig } ppcpConfig={ ppcpConfig }
buttonAttributes={ buttonAttributes }
/> />
); );
} }
@ -89,5 +97,6 @@ registerExpressPaymentMethod( {
canMakePayment: () => buttonData.enabled, canMakePayment: () => buttonData.enabled,
supports: { supports: {
features, features,
style: [ 'height', 'borderRadius' ],
}, },
} ); } );