Apple Pay: Add support for Button Options in the Block Checkout

This commit is contained in:
Daniel Dudzic 2024-11-14 09:25:09 +01:00
parent f51c41d222
commit 088d17e927
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
5 changed files with 233 additions and 68 deletions

View file

@ -92,6 +92,24 @@ class ApplePayButton extends PaymentButton {
*/ */
#product = {}; #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 * @inheritDoc
*/ */
@ -125,7 +143,8 @@ class ApplePayButton 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;
@ -135,7 +154,8 @@ class ApplePayButton extends PaymentButton {
externalHandler, externalHandler,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
contextHandler contextHandler,
buttonAttributes
); );
this.init = this.init.bind( this ); this.init = this.init.bind( this );
@ -220,6 +240,20 @@ class ApplePayButton extends PaymentButton {
'No transactionInfo - missing configure() call?' '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( invalidIf(
() => ! this.contextHandler?.validateContext(), () => ! this.contextHandler?.validateContext(),
`Invalid context handler.` `Invalid context handler.`
@ -229,12 +263,60 @@ class ApplePayButton 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 {TransactionInfo} transactionInfo - Transaction details. * @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.#applePayConfig = apiConfig;
this.#transactionInfo = transactionInfo; this.#transactionInfo = transactionInfo;
this.buttonAttributes = attributes;
this.init();
} }
init() { init() {
@ -321,17 +403,35 @@ class ApplePayButton extends PaymentButton {
applyWrapperStyles() { applyWrapperStyles() {
super.applyWrapperStyles(); super.applyWrapperStyles();
const { height } = this.style; const wrapper = this.wrapperElement;
if ( ! wrapper ) {
return;
}
if ( height ) { // Try stored attributes if current ones are missing
const wrapper = this.wrapperElement; const attributes =
this.buttonAttributes?.height || this.buttonAttributes?.borderRadius
? this.buttonAttributes
: this.#storedButtonAttributes;
wrapper.style.setProperty( // Apply height if available
'--apple-pay-button-height', if ( attributes?.height ) {
`${ height }px` const height = parseInt( attributes.height, 10 );
); if ( ! isNaN( height ) ) {
wrapper.style.setProperty(
'--apple-pay-button-height',
`${ height }px`
);
wrapper.style.height = `${ height }px`;
}
}
wrapper.style.height = `${ height }px`; // Apply border radius if available
if ( attributes?.borderRadius ) {
const borderRadius = parseInt( attributes.borderRadius, 10 );
if ( ! isNaN( borderRadius ) ) {
wrapper.style.borderRadius = `${ borderRadius }px`;
}
} }
} }
@ -342,12 +442,23 @@ class ApplePayButton extends PaymentButton {
addButton() { addButton() {
const { color, type, language } = this.style; 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' ); const button = document.createElement( 'apple-pay-button' );
button.id = 'apple-' + this.wrapperId; button.id = 'apple-' + this.wrapperId;
button.setAttribute( 'buttonstyle', color ); button.setAttribute( 'buttonstyle', color );
button.setAttribute( 'type', type ); button.setAttribute( 'type', type );
button.setAttribute( 'locale', language ); button.setAttribute( 'locale', language );
button.style.display = 'block';
button.addEventListener( 'click', ( evt ) => { button.addEventListener( 'click', ( evt ) => {
evt.preventDefault(); evt.preventDefault();
this.onButtonClick(); this.onButtonClick();

View file

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

View file

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

View file

@ -4,7 +4,12 @@ import usePayPalScript from '../hooks/usePayPalScript';
import useApplepayScript from '../hooks/useApplepayScript'; import useApplepayScript from '../hooks/useApplepayScript';
import useApplepayConfig from '../hooks/useApplepayConfig'; import useApplepayConfig from '../hooks/useApplepayConfig';
const ApplepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => { const ApplepayButton = ( {
namespace,
buttonConfig,
ppcpConfig,
buttonAttributes,
} ) => {
const [ buttonHtml, setButtonHtml ] = useState( '' ); const [ buttonHtml, setButtonHtml ] = useState( '' );
const [ buttonElement, setButtonElement ] = useState( null ); const [ buttonElement, setButtonElement ] = useState( null );
const [ componentFrame, setComponentFrame ] = useState( null ); const [ componentFrame, setComponentFrame ] = useState( null );
@ -31,19 +36,42 @@ const ApplepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => {
namespace, namespace,
buttonConfig, buttonConfig,
ppcpConfig, ppcpConfig,
applepayConfig applepayConfig,
buttonAttributes
); );
useEffect( () => { useEffect( () => {
if ( applepayButton ) { if ( ! applepayButton || ! buttonElement ) {
setButtonHtml( applepayButton.outerHTML ); 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 ( return (
<div <div
ref={ setButtonElement } ref={ setButtonElement }
dangerouslySetInnerHTML={ { __html: buttonHtml } } dangerouslySetInnerHTML={ { __html: buttonHtml } }
style={ {
height: buttonAttributes?.height
? `${ buttonAttributes.height }px`
: undefined,
'--apple-pay-button-height': buttonAttributes?.height
? `${ buttonAttributes.height }px`
: undefined,
borderRadius: buttonAttributes?.borderRadius
? `${ buttonAttributes.borderRadius }px`
: undefined,
overflow: 'hidden',
} }
/> />
); );
}; };

View file

@ -19,7 +19,7 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig; window.PayPalCommerceGateway = ppcpConfig;
} }
const ApplePayComponent = ( { isEditing } ) => { const ApplePayComponent = ( { isEditing, buttonAttributes } ) => {
const [ paypalLoaded, setPaypalLoaded ] = useState( false ); const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ applePayLoaded, setApplePayLoaded ] = useState( false ); const [ applePayLoaded, setApplePayLoaded ] = useState( false );
const wrapperRef = useRef( null ); const wrapperRef = useRef( null );
@ -57,8 +57,13 @@ const ApplePayComponent = ( { isEditing } ) => {
buttonConfig.reactWrapper = wrapperRef.current; buttonConfig.reactWrapper = wrapperRef.current;
new ManagerClass( namespace, buttonConfig, ppcpConfig ); new ManagerClass(
}, [ paypalLoaded, applePayLoaded, isEditing ] ); namespace,
buttonConfig,
ppcpConfig,
buttonAttributes
);
}, [ paypalLoaded, applePayLoaded, isEditing, buttonAttributes ] );
if ( isEditing ) { if ( isEditing ) {
return ( return (
@ -66,6 +71,7 @@ const ApplePayComponent = ( { isEditing } ) => {
namespace={ namespace } namespace={ namespace }
buttonConfig={ buttonConfig } buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig } ppcpConfig={ ppcpConfig }
buttonAttributes={ buttonAttributes }
/> />
); );
} }