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

Apple Pay: Add support for Button Options in the Block Checkout (3827)
This commit is contained in:
Emili Castells 2024-11-20 11:00:28 +01:00 committed by GitHub
commit a83dffa35d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 238 additions and 64 deletions

View file

@ -92,6 +92,24 @@ class ApplePayButton extends PaymentButton {
*/
#product = {};
/**
* 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 {Object|null}
*/
#storedButtonAttributes = null;
/**
* @inheritDoc
*/
@ -125,7 +143,8 @@ class ApplePayButton extends PaymentButton {
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
contextHandler,
buttonAttributes
) {
// Disable debug output in the browser console:
// buttonConfig.is_debug = false;
@ -135,7 +154,8 @@ class ApplePayButton extends PaymentButton {
externalHandler,
buttonConfig,
ppcpConfig,
contextHandler
contextHandler,
buttonAttributes
);
this.init = this.init.bind( this );
@ -220,6 +240,20 @@ class ApplePayButton extends PaymentButton {
'No transactionInfo - missing configure() call?'
);
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'
);
invalidIf(
() => ! this.contextHandler?.validateContext(),
`Invalid context handler.`
@ -229,12 +263,60 @@ class ApplePayButton extends PaymentButton {
/**
* Configures the button instance. Must be called before the initial `init()`.
*
* @param {Object} apiConfig - API configuration.
* @param {TransactionInfo} transactionInfo - Transaction details.
* @param {Object} apiConfig - API configuration.
* @param {TransactionInfo} transactionInfo - Transaction details.
* @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(
'ApplePay: Timeout waiting for buttonAttributes - proceeding with initialization'
);
this.#applePayConfig = apiConfig;
this.#transactionInfo = transactionInfo;
this.buttonAttributes = attributes || buttonAttributes;
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.#applePayConfig = apiConfig;
this.#transactionInfo = transactionInfo;
this.buttonAttributes = attributes;
this.init();
}
init() {
@ -321,17 +403,43 @@ class ApplePayButton extends PaymentButton {
applyWrapperStyles() {
super.applyWrapperStyles();
const { height } = this.style;
const wrapper = this.wrapperElement;
if ( ! wrapper ) {
return;
}
if ( height ) {
const wrapper = this.wrapperElement;
// Try stored attributes if current ones are missing
const attributes =
this.buttonAttributes?.height || this.buttonAttributes?.borderRadius
? this.buttonAttributes
: this.#storedButtonAttributes;
const defaultHeight = 48;
const defaultBorderRadius = 4;
const height = attributes?.height
? parseInt( attributes.height, 10 )
: defaultHeight;
if ( ! isNaN( height ) ) {
wrapper.style.setProperty(
'--apple-pay-button-height',
`${ height }px`
);
wrapper.style.height = `${ height }px`;
} else {
wrapper.style.setProperty(
'--apple-pay-button-height',
`${ defaultHeight }px`
);
wrapper.style.height = `${ defaultHeight }px`;
}
const borderRadius = attributes?.borderRadius
? parseInt( attributes.borderRadius, 10 )
: defaultBorderRadius;
if ( ! isNaN( borderRadius ) ) {
wrapper.style.borderRadius = `${ borderRadius }px`;
}
}
@ -342,12 +450,23 @@ class ApplePayButton extends PaymentButton {
addButton() {
const { color, type, language } = this.style;
// If current buttonAttributes are missing, try to use stored ones
if (
! this.buttonAttributes?.height &&
this.#storedButtonAttributes?.height
) {
this.buttonAttributes = { ...this.#storedButtonAttributes };
}
const button = document.createElement( 'apple-pay-button' );
button.id = 'apple-' + this.wrapperId;
button.setAttribute( 'buttonstyle', color );
button.setAttribute( 'type', type );
button.setAttribute( 'locale', language );
button.style.display = 'block';
button.addEventListener( 'click', ( evt ) => {
evt.preventDefault();
this.onButtonClick();

View file

@ -3,65 +3,83 @@ import ApplePayButton from './ApplepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
class ApplePayManager {
#namespace = '';
#buttonConfig = null;
#ppcpConfig = null;
#applePayConfig = null;
#contextHandler = null;
#transactionInfo = null;
#buttons = [];
constructor( namespace, buttonConfig, ppcpConfig, buttonAttributes = {} ) {
this.namespace = namespace;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.buttonAttributes = buttonAttributes;
this.applePayConfig = null;
this.transactionInfo = null;
this.contextHandler = null;
constructor( namespace, buttonConfig, ppcpConfig ) {
this.#namespace = namespace;
this.#buttonConfig = buttonConfig;
this.#ppcpConfig = ppcpConfig;
this.buttons = [];
this.onContextBootstrap = this.onContextBootstrap.bind( this );
buttonModuleWatcher.watchContextBootstrap( this.onContextBootstrap );
}
buttonModuleWatcher.watchContextBootstrap( async ( bootstrap ) => {
this.contextHandler = ContextHandlerFactory.create(
bootstrap.context,
buttonConfig,
ppcpConfig,
bootstrap.handler
);
async onContextBootstrap( bootstrap ) {
this.#contextHandler = ContextHandlerFactory.create(
bootstrap.context,
this.#buttonConfig,
this.#ppcpConfig,
bootstrap.handler
);
const button = ApplePayButton.createButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
ppcpConfig,
this.contextHandler,
this.buttonAttributes
);
const button = ApplePayButton.createButton(
bootstrap.context,
bootstrap.handler,
this.#buttonConfig,
this.#ppcpConfig,
this.#contextHandler
);
this.buttons.push( button );
const initButton = () => {
button.configure(
this.applePayConfig,
this.transactionInfo,
this.buttonAttributes
);
button.init();
};
this.#buttons.push( button );
// Initialize button only if applePayConfig and transactionInfo are already fetched.
if ( this.applePayConfig && this.transactionInfo ) {
initButton();
} else {
// Ensure ApplePayConfig is loaded before proceeding.
await this.init();
// Ensure ApplePayConfig is loaded before proceeding.
await this.init();
button.configure( this.#applePayConfig, this.#transactionInfo );
button.init();
if ( this.applePayConfig && this.transactionInfo ) {
initButton();
}
}
} );
}
async init() {
try {
if ( ! this.#applePayConfig ) {
this.#applePayConfig = await window[ this.#namespace ]
if ( ! this.applePayConfig ) {
// Gets ApplePay configuration of the PayPal merchant.
this.applePayConfig = await window[ this.namespace ]
.Applepay()
.config();
if ( ! this.#applePayConfig ) {
console.error( 'No ApplePayConfig received during init' );
}
}
if ( ! this.#transactionInfo ) {
this.#transactionInfo = await this.fetchTransactionInfo();
if ( ! this.transactionInfo ) {
this.transactionInfo = await this.fetchTransactionInfo();
}
if ( ! this.#applePayConfig ) {
console.error( 'No transactionInfo found during init' );
if ( ! this.applePayConfig ) {
console.error( 'No ApplePayConfig received during init' );
} else if ( ! this.transactionInfo ) {
console.error( 'No transactionInfo found during init' );
} else {
for ( const button of this.buttons ) {
button.configure(
this.applePayConfig,
this.transactionInfo,
this.buttonAttributes
);
button.init();
}
}
} catch ( error ) {
@ -71,10 +89,10 @@ class ApplePayManager {
async fetchTransactionInfo() {
try {
if ( ! this.#contextHandler ) {
if ( ! this.contextHandler ) {
throw new Error( 'ContextHandler is not initialized' );
}
return await this.#contextHandler.transactionInfo();
return await this.contextHandler.transactionInfo();
} catch ( error ) {
console.error( 'Error fetching transaction info:', error );
throw error;
@ -82,7 +100,7 @@ class ApplePayManager {
}
reinit() {
for ( const button of this.#buttons ) {
for ( const button of this.buttons ) {
button.reinit();
}
}

View file

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

View file

@ -4,7 +4,12 @@ import usePayPalScript from '../hooks/usePayPalScript';
import useApplepayScript from '../hooks/useApplepayScript';
import useApplepayConfig from '../hooks/useApplepayConfig';
const ApplepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => {
const ApplepayButton = ( {
namespace,
buttonConfig,
ppcpConfig,
buttonAttributes,
} ) => {
const [ buttonHtml, setButtonHtml ] = useState( '' );
const [ buttonElement, setButtonElement ] = useState( null );
const [ componentFrame, setComponentFrame ] = useState( null );
@ -31,19 +36,42 @@ const ApplepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => {
namespace,
buttonConfig,
ppcpConfig,
applepayConfig
applepayConfig,
buttonAttributes
);
useEffect( () => {
if ( applepayButton ) {
setButtonHtml( applepayButton.outerHTML );
if ( ! applepayButton || ! buttonElement ) {
return;
}
}, [ applepayButton ] );
setButtonHtml( applepayButton.outerHTML );
// Add timeout to ensure button is displayed after render
setTimeout( () => {
const button = buttonElement.querySelector( 'apple-pay-button' );
if ( button ) {
button.style.display = 'block';
}
}, 100 ); // Add a small delay to ensure DOM is ready
}, [ applepayButton, buttonElement ] );
return (
<div
ref={ setButtonElement }
dangerouslySetInnerHTML={ { __html: buttonHtml } }
style={ {
height: buttonAttributes?.height
? `${ buttonAttributes.height }px`
: '48px',
'--apple-pay-button-height': buttonAttributes?.height
? `${ buttonAttributes.height }px`
: '48px',
borderRadius: buttonAttributes?.borderRadius
? `${ buttonAttributes.borderRadius }px`
: undefined,
overflow: 'hidden',
} }
/>
);
};

View file

@ -19,7 +19,7 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
}
const ApplePayComponent = ( { isEditing } ) => {
const ApplePayComponent = ( { isEditing, buttonAttributes } ) => {
const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ applePayLoaded, setApplePayLoaded ] = useState( false );
const wrapperRef = useRef( null );
@ -57,8 +57,13 @@ const ApplePayComponent = ( { isEditing } ) => {
buttonConfig.reactWrapper = wrapperRef.current;
new ManagerClass( namespace, buttonConfig, ppcpConfig );
}, [ paypalLoaded, applePayLoaded, isEditing ] );
new ManagerClass(
namespace,
buttonConfig,
ppcpConfig,
buttonAttributes
);
}, [ paypalLoaded, applePayLoaded, isEditing, buttonAttributes ] );
if ( isEditing ) {
return (
@ -66,6 +71,7 @@ const ApplePayComponent = ( { isEditing } ) => {
namespace={ namespace }
buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig }
buttonAttributes={ buttonAttributes }
/>
);
}
@ -102,5 +108,6 @@ registerExpressPaymentMethod( {
canMakePayment: () => buttonData.enabled,
supports: {
features,
style: [ 'height', 'borderRadius' ],
},
} );