Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Express Checkout button previews in editor #10141

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
63184d1
Show Express Checkout button previews in editor when the elements do …
danielmx-dev Jan 13, 2025
ff760c9
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Jan 16, 2025
b2837a7
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Jan 16, 2025
432fd73
Add changelog
danielmx-dev Jan 16, 2025
f2ee578
Rename component
danielmx-dev Jan 16, 2025
36a4544
Use google pay library directly to render test buttons
danielmx-dev Jan 16, 2025
5e94840
Add onClick parameter
danielmx-dev Jan 16, 2025
1fe5984
Apply border radius changes
danielmx-dev Jan 16, 2025
2d27578
Use correct reference
danielmx-dev Jan 16, 2025
60f6cb2
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
frosso Jan 17, 2025
b087475
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
frosso Jan 21, 2025
eca4c98
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Jan 22, 2025
8b9986c
Refactor hooks
danielmx-dev Jan 22, 2025
987368a
Add button types to the previews
danielmx-dev Jan 22, 2025
bc6a1a0
Always display the preview component when isPreview is true
danielmx-dev Jan 22, 2025
aaa14f8
Remove unnecessary styles
danielmx-dev Jan 22, 2025
d327efb
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Jan 24, 2025
3fa843e
Fix issue with outline theme
danielmx-dev Jan 24, 2025
0301252
Separate components
danielmx-dev Jan 24, 2025
d22ea91
Remove conditional assignment for onClick handler
danielmx-dev Jan 24, 2025
13de3e2
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Jan 28, 2025
f26c0ad
Only use the preview components if the express payment method is supp…
danielmx-dev Jan 30, 2025
5babdc1
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Jan 30, 2025
b0abd9b
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Feb 14, 2025
a86db03
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Feb 21, 2025
8c754f1
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Feb 27, 2025
120794a
Implement previews for non-tokenized ECE
danielmx-dev Feb 27, 2025
ab4eb29
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
danielmx-dev Feb 27, 2025
8426805
Merge branch 'develop' into fix/show-preview-for-express-checkout-but…
frosso Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: fix

Show Express Checkout button previews in template editor
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* External dependencies
*/
import { useEffect, useMemo, useRef } from 'react';

/**
* Internal dependencies
*/
import { getExpressCheckoutButtonAppearance } from 'wcpay/express-checkout/utils';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this file is within the tokenized-express-checkout, it's best to include the functions from that directory, rather than the express-checkout directory (which doesn't include "tokenized" cart changes).

Otherwise the bundles are going to get mixed, and there might be modifications in the "tokenized" functions that wouldn't be considered.

Suggested change
import { getExpressCheckoutButtonAppearance } from 'wcpay/express-checkout/utils';
import { getExpressCheckoutButtonAppearance } from '../../utils';


const ExpressCheckoutButtonPreview = ( {
expressPaymentMethod,
options,
buttonAttributes,
} ) => {
const appearance = useMemo(
() => getExpressCheckoutButtonAppearance( buttonAttributes ),
[ buttonAttributes ]
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar observation as the one I made in ExpressCheckoutComponent - is there a scenario where this useMemo would be useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this scenario I did it to avoid recalculating the border radius with every render, but the savings are probably negligible, I'll just remove it and see how it performs.

const ref = useRef( null );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use a less generic ref name for this variable?
It looks like it's used for the GooglePlay's container - maybe googlePlayContainerRef or googlePlayWrapperRef?

const renderGooglePayButtonPromise = useRef( null );

const theme = options.buttonTheme[ expressPaymentMethod ];
const borderRadius = appearance.variables.borderRadius;

useEffect( () => {
if (
ref.current &&
expressPaymentMethod === 'googlePay' &&
! renderGooglePayButtonPromise.current
) {
renderGooglePayButtonPromise.current = ( async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about the assignment of this function to renderGooglePayButtonPromise.current - could we have gotten away with just assigning a flag to indicate if the loading had been called?

i.e.: something like this:

diff --git a/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js b/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js
index b612e0202..1fce7c0d4 100644
--- a/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js
+++ b/client/tokenized-express-checkout/blocks/components/express-checkout-button-preview.js
@@ -18,7 +18,7 @@ const ExpressCheckoutButtonPreview = ( {
 		[ buttonAttributes ]
 	);
 	const ref = useRef( null );
-	const renderGooglePayButtonPromise = useRef( null );
+	const hasStartedLoadingGooglePlayButton = useRef( false );
 
 	const theme = options.buttonTheme[ expressPaymentMethod ];
 	const borderRadius = appearance.variables.borderRadius;
@@ -27,9 +27,10 @@ const ExpressCheckoutButtonPreview = ( {
 		if (
 			ref.current &&
 			expressPaymentMethod === 'googlePay' &&
-			! renderGooglePayButtonPromise.current
+			! hasStartedLoadingGooglePlayButton.current
 		) {
-			renderGooglePayButtonPromise.current = ( async () => {
+			hasStartedLoadingGooglePlayButton.current = true;
+			( async () => {
 				const targetDocument = ref.current.ownerDocument;
 				const targetWindow = targetDocument.defaultView;
 				if ( ! targetWindow.google?.payments?.api?.PaymentsClient ) {

const targetDocument = ref.current.ownerDocument;
const targetWindow = targetDocument.defaultView;
if ( ! targetWindow.google?.payments?.api?.PaymentsClient ) {
await new Promise( ( resolve ) => {
const script = document.createElement( 'script' );
script.src = 'https://pay.google.com/gp/p/js/pay.js';
script.onload = resolve;
targetDocument.head.appendChild( script );
} );
}

const googlePayClient = new targetWindow.google.payments.api.PaymentsClient(
{
environment: 'TEST',
}
);

const buttonColor = theme === 'black' ? 'black' : 'white'; // There is no 'outline' theme in Google Pay.

const button = googlePayClient.createButton( {
buttonType: 'plain',
buttonColor,
buttonRadius: parseFloat( borderRadius ),
buttonSizeMode: 'fill',
onClick: () => {},
} );
ref.current.appendChild( button );
} )();
}
}, [ ref, theme, expressPaymentMethod, borderRadius ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, [ ref, theme, expressPaymentMethod, borderRadius ] );
}, [ theme, expressPaymentMethod, borderRadius ] );

Return values from useRefs shouldn't be used as part of hook dependencies.


useEffect( () => {
ref.current
?.querySelector( 'button' )
?.style?.setProperty( 'border-radius', borderRadius );
}, [ ref, borderRadius ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, [ ref, borderRadius ] );
}, [ borderRadius ] );


if ( expressPaymentMethod === 'googlePay' ) {
return (
<div
ref={ ref }
style={ {
height: `${ options.buttonHeight }px`,
width: '100%',
} }
/>
);
}

const buttonStyle = {
height: `${ options.buttonHeight }px`,
borderRadius,
};

if ( expressPaymentMethod === 'applePay' ) {
buttonStyle.WebkitAppearance = '-apple-pay-button';
if ( theme === 'black' ) {
buttonStyle.ApplePayButtonStyle = 'black';
} else if ( theme === 'outline' ) {
buttonStyle.ApplePayButtonStyle = 'white-outline';
} else {
buttonStyle.ApplePayButtonStyle = 'white';
}

return (
<div>
<button
type="button"
id={ `express-checkout-button-preview-${ expressPaymentMethod }` }
className="express-checkout-button-preview"
style={ buttonStyle }
/>
</div>
);
}

return null;
};

export default ExpressCheckoutButtonPreview;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { useEffect, useMemo, useRef, useState } from 'react';
import { ExpressCheckoutElement } from '@stripe/react-stripe-js';
/**
* Internal dependencies
Expand All @@ -11,6 +12,9 @@ import {
} from '../../event-handlers';
import { useExpressCheckout } from '../hooks/use-express-checkout';
import { PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT } from 'wcpay/checkout/constants';
import ExpressCheckoutButtonPreview from './express-checkout-button-preview';

const FALLBACK_BUTTON_WAIT_TIME = 3000; // 3 seconds

const getPaymentMethodsOverride = ( enabledPaymentMethod ) => {
const allDisabled = {
Expand Down Expand Up @@ -93,6 +97,8 @@ const ExpressCheckoutComponent = ( {
onClose,
setExpressPaymentError,
} );
const [ showFallbackButton, setShowFallbackButton ] = useState( false );
const onElementsReadyCalled = useRef( false );
const onClickHandler = ! isPreview ? onButtonClick : () => {};
const onShippingAddressChange = ( event ) =>
shippingAddressChangeHandler( event, elements );
Expand All @@ -101,6 +107,7 @@ const ExpressCheckoutComponent = ( {
shippingRateChangeHandler( event, elements );

const onElementsReady = ( event ) => {
onElementsReadyCalled.current = true;
const paymentMethodContainer = document.getElementById(
`express-payment-method-${ PAYMENT_METHOD_NAME_EXPRESS_CHECKOUT_ELEMENT }_${ expressPaymentMethod }`
);
Expand All @@ -118,29 +125,53 @@ const ExpressCheckoutComponent = ( {
onReady( event );
};

// The Cart & Checkout blocks provide unified styles across all buttons,
// which should override the extension specific settings.
const withBlockOverride = () => {
const override = {};
if ( typeof buttonAttributes !== 'undefined' ) {
override.buttonHeight = Number( buttonAttributes.height );
}
const checkoutElementOptions = useMemo( () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure useMemo is too useful, in this scenario. It seems that the memoized value would be discarded at each re-render, regardless.

buttonOptions comes from the useExpressCheckout hook. And, in turn, it is computed by the getExpressCheckoutButtonStyleSettings() utility.

The getExpressCheckoutButtonStyleSettings utility returns a new object each time it is invoked.

So, buttonOptions will have a new in-memory reference each time the hook/utility is invoked, making useMemo also execute each time the ExpressCheckoutComponent component re-renders.

I noticed that buttonAttributes prop is also re-computed at each re-render: https://github.com/woocommerce/woocommerce/blob/3fced787bf20112dae420cb97b3da617a929b7de/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx#L33-L38

Is there a specific scenario you were thinking useMemo would have been useful in this component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was leftover from when why was trying another approach. I'll remove the useMemo usage.

// The Cart & Checkout blocks provide unified styles across all buttons,
// which should override the extension specific settings.
const withBlockOverride = () => {
const override = {};
if ( typeof buttonAttributes !== 'undefined' ) {
override.buttonHeight = Number( buttonAttributes.height );
}
return {
...buttonOptions,
...override,
};
};
return {
...buttonOptions,
...override,
...withBlockOverride(),
...adjustButtonHeights( withBlockOverride(), expressPaymentMethod ),
...getPaymentMethodsOverride( expressPaymentMethod ),
};
};
}, [ expressPaymentMethod, buttonAttributes, buttonOptions ] );

useEffect( () => {
if ( ! isPreview || onElementsReadyCalled.current ) {
return;
}

const handle = setTimeout( () => {
if ( ! onElementsReadyCalled.current ) {
setShowFallbackButton( true );
}
}, FALLBACK_BUTTON_WAIT_TIME );

return () => clearTimeout( handle );
}, [ isPreview, onElementsReadyCalled ] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}, [ isPreview, onElementsReadyCalled ] );
}, [ isPreview ] );

The value returned by useRef shouldn't be a hook dependency.


if ( showFallbackButton ) {
return (
<ExpressCheckoutButtonPreview
expressPaymentMethod={ expressPaymentMethod }
buttonAttributes={ buttonAttributes }
options={ checkoutElementOptions }
/>
);
}

return (
<ExpressCheckoutElement
options={ {
...withBlockOverride(),
...adjustButtonHeights(
withBlockOverride(),
expressPaymentMethod
),
...getPaymentMethodsOverride( expressPaymentMethod ),
} }
options={ checkoutElementOptions }
onClick={ onClickHandler }
onConfirm={ onConfirm }
onReady={ onElementsReady }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,25 @@
margin-left: 1px !important;
width: 99% !important;
}

// Preview button
@supports not ( -webkit-appearance: -apple-pay-button ) {
/* stylelint-disable-next-line selector-id-pattern */
#express-payment-method-woocommerce_payments_express_checkout_applePay:has( #express-checkout-button-preview-applePay ) {
display: none;
}
}

.express-checkout-button-preview {
width: 100%;
background-repeat: no-repeat;
background-position: center;
background-origin: content-box;
background-size: contain;
padding: 11px 15%;
box-sizing: border-box;
box-shadow: none;
border: none;
outline: 1px solid #3c4043;
outline-offset: -1px;
}
Loading