Merge pull request #2772 from woocommerce/PCP-3827-implement-block-button-styles-for-google-pay-button

Google Pay: Add support for Button Options in the Block Checkout (3827)
This commit is contained in:
Danny Dudzic 2024-11-14 21:24:26 +01:00 committed by GitHub
commit f628cffa9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 305 additions and 93 deletions

View file

@ -172,6 +172,11 @@ export default class PaymentButton {
*/
#contextHandler;
/**
* Button attributes.
*/
#buttonAttributes;
/**
* Whether the current browser/website support the payment method.
*
@ -211,11 +216,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(
@ -223,7 +229,8 @@ export default class PaymentButton {
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
contextHandler,
buttonAttributes
) {
const buttonInstances = getInstances();
const instanceKey = `${ this.methodId }.${ context }`;
@ -234,7 +241,8 @@ export default class PaymentButton {
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
contextHandler,
buttonAttributes
);
buttonInstances.set( instanceKey, button );
@ -278,18 +286,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' );
@ -307,6 +317,7 @@ export default class PaymentButton {
this.#ppcpConfig = ppcpConfig;
this.#externalHandler = externalHandler;
this.#contextHandler = contextHandler;
this.#buttonAttributes = buttonAttributes;
this.#logger = new ConsoleLogger( methodName, context );
@ -921,15 +932,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 )
@ -1007,7 +1023,7 @@ export default class PaymentButton {
this.removeButton();
}
this.log( 'addButton', button );
this.log( 'insertButton', button );
this.#button = button;
wrapper.appendChild( this.#button );

View file

@ -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 */

View file

@ -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 =
'<style>.block-editor-iframe__html .gpay-card-info-animated-progress-bar-container {display:none !important}</style>';
setButtonHtml( googlepayButton.outerHTML + hideLoader );
}
}, [ googlepayButton ] );
return (
<div
ref={ setButtonElement }
dangerouslySetInnerHTML={ { __html: buttonHtml } }
/>
<>
<div
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 { 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;

View file

@ -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;

View file

@ -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 );
@ -240,29 +274,95 @@ class GooglepayButton extends PaymentButton {
() => ! this.contextHandler?.validateContext(),
`Invalid context handler.`
);
invalidIf(
() =>
this.buttonAttributes?.height &&
isNaN( parseInt( this.buttonAttributes.height ) ),
'Invalid height in buttonAttributes'
);
invalidIf(
() =>
this.buttonAttributes?.borderRadius &&
isNaN( parseInt( this.buttonAttributes.borderRadius ) ),
'Invalid borderRadius in buttonAttributes'
);
return true;
}
/**
* 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;
}
@ -270,16 +370,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(
@ -357,30 +447,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;
}
//------------------------

View file

@ -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();
}

View file

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

View file

@ -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' ],
},
} );