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

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from

Conversation

danielmx-dev
Copy link
Contributor

@danielmx-dev danielmx-dev commented Jan 13, 2025

Fixes #10043

Changes proposed in this Pull Request

When visiting the Page: Cart template, the button for express payment is not visible in the Cart block. When debugging this further, I noticed that when using the template editor, the contents were rendered inside an iframe, which Gutenberg does by design: WordPress/gutenberg#25775

When the Stripe iframe is located inside the FSE iframe, no callbacks are executed and the payment buttons are never loaded (even if the conditions to render the buttons are met). This is not a problem with the page editor since, in my testing, that editor does not use an iframe to render the contents of the page, so the buttons load as expected.

In this PR I'm proposing to establish a timeout, if the elementsReady callback is never called, we render fallback buttons using the APIs provided by Google Pay (the Pay javascript library and https://developers.google.com/pay/api/web/guides/brand-guidelines ) and ApplePay (proprietary CSS rules only available in Safari https://developer.apple.com/documentation/apple_pay_on_the_web/displaying_apple_pay_buttons_using_css).

The downside of this approach is that, if more express payment methods are added, we'd need to implement a way to render the buttons ourselves. An alternative would be to just show placeholder buttons and don't worry too much about the styling, but please let me know what do you think.

Testing instructions

  • Make sure you are using the Cart block in your designated Cart page (The designated Cart page is configured in WooCommerce > Settings > Advanced > Page setup).
  • Edit the cart page in the Pages editor.
  • In a new tab, using the twenty-twenty four theme, go to Appearance > Editor > Templates and then edit the Page: Cart template.

Previous Result:
In the page editor, the supported Express Checkout buttons are rendered
image

In the template editor no buttons are rendered, and there's only a blank space:
image

Current Result (using this branch):
The Page editor remains the same, and buttons are rendered:
image

The Template editor now renders the express payment buttons:
image

Regression Testing:

Express Payment method should still work as expected for making purchases in the Product, Cart, and Checkout pages.


  • Run npm run changelog to add a changelog file, choose patch to leave it empty if the change is not significant. You can add multiple changelog files in one PR by running this command a few times.
  • Covered with tests (or have a good reason not to test in description ☝️)
  • Tested on mobile (or does not apply)

Post merge

@botwoo
Copy link
Collaborator

botwoo commented Jan 13, 2025

Test the build

Option 1. Jetpack Beta

  • Install and activate Jetpack Beta.
  • Use this build by searching for PR number 10141 or branch name fix/show-preview-for-express-checkout-buttons-in-editor in your-test.site/wp-admin/admin.php?page=jetpack-beta&plugin=woocommerce-payments

Option 2. Jurassic Ninja - available for logged-in A12s

🚀 Launch a JN site with this branch 🚀

ℹ️ Install this Tampermonkey script to get more options.


Build info:

  • Latest commit: d22ea91
  • Build time: 2025-01-24 17:52:40 UTC

Note: the build is updated when a new commit is pushed to this PR.

Copy link
Contributor

github-actions bot commented Jan 13, 2025

Size Change: +719 B (0%)

Total Size: 1.36 MB

Filename Size Change
release/woocommerce-payments/dist/blocks-checkout-rtl.css 2.72 kB +74 B (+3%)
release/woocommerce-payments/dist/blocks-checkout.css 2.72 kB +74 B (+3%)
release/woocommerce-payments/dist/blocks-checkout.js 56.2 kB +571 B (+1%)
ℹ️ View Unchanged
Filename Size
release/woocommerce-payments/assets/css/admin.css 1.37 kB
release/woocommerce-payments/assets/css/admin.rtl.css 1.37 kB
release/woocommerce-payments/assets/css/success.css 182 B
release/woocommerce-payments/assets/css/success.rtl.css 184 B
release/woocommerce-payments/dist/cart-block.js 17.2 kB
release/woocommerce-payments/dist/cart.js 5.73 kB
release/woocommerce-payments/dist/checkout-rtl.css 1.13 kB
release/woocommerce-payments/dist/checkout.css 1.13 kB
release/woocommerce-payments/dist/checkout.js 33.6 kB
release/woocommerce-payments/dist/express-checkout-rtl.css 229 B
release/woocommerce-payments/dist/express-checkout.css 229 B
release/woocommerce-payments/dist/express-checkout.js 15.7 kB
release/woocommerce-payments/dist/frontend-tracks.js 854 B
release/woocommerce-payments/dist/index-rtl.css 39.6 kB
release/woocommerce-payments/dist/index.css 39.5 kB
release/woocommerce-payments/dist/index.js 301 kB
release/woocommerce-payments/dist/multi-currency-analytics.js 1.08 kB
release/woocommerce-payments/dist/multi-currency-rtl.css 4.47 kB
release/woocommerce-payments/dist/multi-currency-switcher-block.js 61.1 kB
release/woocommerce-payments/dist/multi-currency.css 4.47 kB
release/woocommerce-payments/dist/multi-currency.js 57.8 kB
release/woocommerce-payments/dist/order-rtl.css 730 B
release/woocommerce-payments/dist/order.css 730 B
release/woocommerce-payments/dist/order.js 42.5 kB
release/woocommerce-payments/dist/payment-gateways-rtl.css 1.33 kB
release/woocommerce-payments/dist/payment-gateways.css 1.33 kB
release/woocommerce-payments/dist/payment-gateways.js 38.9 kB
release/woocommerce-payments/dist/plugins-page-rtl.css 386 B
release/woocommerce-payments/dist/plugins-page.css 386 B
release/woocommerce-payments/dist/plugins-page.js 20.1 kB
release/woocommerce-payments/dist/product-details-rtl.css 433 B
release/woocommerce-payments/dist/product-details.css 436 B
release/woocommerce-payments/dist/product-details.js 12.5 kB
release/woocommerce-payments/dist/settings-rtl.css 11.6 kB
release/woocommerce-payments/dist/settings.css 11.5 kB
release/woocommerce-payments/dist/settings.js 224 kB
release/woocommerce-payments/dist/subscription-edit-page.js 703 B
release/woocommerce-payments/dist/subscription-product-onboarding-modal-rtl.css 524 B
release/woocommerce-payments/dist/subscription-product-onboarding-modal.css 524 B
release/woocommerce-payments/dist/subscription-product-onboarding-modal.js 20.2 kB
release/woocommerce-payments/dist/subscription-product-onboarding-toast.js 730 B
release/woocommerce-payments/dist/subscriptions-empty-state-rtl.css 120 B
release/woocommerce-payments/dist/subscriptions-empty-state.css 120 B
release/woocommerce-payments/dist/subscriptions-empty-state.js 19.3 kB
release/woocommerce-payments/dist/tokenized-express-checkout-rtl.css 229 B
release/woocommerce-payments/dist/tokenized-express-checkout.css 229 B
release/woocommerce-payments/dist/tokenized-express-checkout.js 16.6 kB
release/woocommerce-payments/dist/tos-rtl.css 235 B
release/woocommerce-payments/dist/tos.css 235 B
release/woocommerce-payments/dist/tos.js 21.8 kB
release/woocommerce-payments/dist/woopay-direct-checkout.js 6.13 kB
release/woocommerce-payments/dist/woopay-express-button.js 25 kB
release/woocommerce-payments/dist/woopay-rtl.css 4.31 kB
release/woocommerce-payments/dist/woopay.css 4.28 kB
release/woocommerce-payments/dist/woopay.js 71 kB
release/woocommerce-payments/includes/subscriptions/assets/css/plugin-page.css 625 B
release/woocommerce-payments/includes/subscriptions/assets/js/plugin-page.js 814 B
release/woocommerce-payments/vendor/automattic/jetpack-assets/build/i18n-loader.js 2.46 kB
release/woocommerce-payments/vendor/automattic/jetpack-assets/build/jetpack-script-data.js 772 B
release/woocommerce-payments/vendor/automattic/jetpack-assets/src/js/i18n-loader.js 1.02 kB
release/woocommerce-payments/vendor/automattic/jetpack-assets/src/js/script-data.js 69 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/babel.config.js 163 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/identity-crisis.css 2.47 kB
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/identity-crisis.js 14.2 kB
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/identity-crisis.rtl.css 2.47 kB
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-connection.css 10 kB
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-connection.js 28.4 kB
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-connection.rtl.css 10 kB
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-admin-create-user.css 198 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-admin-create-user.js 280 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-admin-create-user.rtl.css 198 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-login.css 625 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-login.js 333 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-login.rtl.css 626 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/jetpack-sso-users.js 424 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/tracks-ajax.js 521 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/dist/tracks-callables.js 585 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-admin-create-user.css 215 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-admin-create-user.js 521 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-login.css 721 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-login.js 412 B
release/woocommerce-payments/vendor/automattic/jetpack-connection/src/sso/jetpack-sso-users.js 632 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/about.css 1.04 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/admin-empty-state.css 294 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/admin-order-statuses.css 408 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/admin.css 3.59 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/checkout.css 301 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/modal.css 746 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/view-subscription.css 574 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/css/wcs-upgrade.css 414 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/admin-pointers.js 543 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/admin.js 9.4 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/jstz.js 6.78 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/jstz.min.js 3.84 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/meta-boxes-coupon.js 545 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/meta-boxes-subscription.js 2.52 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/moment.js 22.2 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/moment.min.js 11.7 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/payment-method-restrictions.js 1.29 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/admin/wcs-meta-boxes-order.js 507 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/payment-methods.js 358 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/single-product.js 428 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/view-subscription.js 1.38 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/frontend/wcs-cart.js 782 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/modal.js 1.09 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/assets/js/wcs-upgrade.js 1.26 kB
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/build/index.css 391 B
release/woocommerce-payments/vendor/woocommerce/subscriptions-core/build/index.js 3.04 kB

compressed-size-action

@danielmx-dev danielmx-dev marked this pull request as ready for review January 16, 2025 19:07
@danielmx-dev danielmx-dev requested review from a team and frosso and removed request for a team January 16, 2025 19:47
Copy link
Contributor

@frosso frosso left a comment

Choose a reason for hiding this comment

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

A few comments about memoization and dependencies I thought would be worth pointing out.

The fallback works well 👍 which makes me wonder - should we just use this fallback in preview mode, without worrying about the Stripe "preview" (which is not really a "preview")?

}, 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 ( 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.

/**
* 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';

Comment on lines 16 to 19
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.

() => getExpressCheckoutButtonAppearance( buttonAttributes ),
[ buttonAttributes ]
);
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?

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.

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 ] );

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 ) {

@danielmx-dev
Copy link
Contributor Author

The fallback works well 👍 which makes me wonder - should we just use this fallback in preview mode, without worrying about the Stripe "preview" (which is not really a "preview")?

My initial reasoning was to always try using Stripe first to provide a Preview as close as possible to the live experience; however, at least from my testing, the appearance in both are really close, so it's probably fine to use the new component in all previews.

@danielmx-dev danielmx-dev requested a review from frosso January 22, 2025 18:26
Copy link
Contributor

@frosso frosso left a comment

Choose a reason for hiding this comment

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

Excellent work! Thank you for entertaining my suggestions.
I added a very optional suggestion to simplify the body of the ExpressCheckoutButtonPreview component.

Now that we have ExpressCheckoutButtonPreview, I am even questioning whether ExpressCheckoutButtonPreview should be rendered within ExpressCheckoutComponent. In the edit property of tokenizedExpressCheckoutElementApplePay/tokenizedExpressCheckoutElementGooglePay we could just render ExpressCheckoutButtonPreview and get rid of the whole isPreview prop 🤷 but I'm guessing that would be too many changes in this PR.

Regardless, this already fixes the main issue 👍

...getPaymentMethodsOverride( expressPaymentMethod ),
};

if ( isPreview ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that isPreview is used in this scenario, I think we can also remove the conditional assignment of the onClickHandler

diff --git a/client/tokenized-express-checkout/blocks/components/express-checkout-component.js b/client/tokenized-express-checkout/blocks/components/express-checkout-component.js
index 84a53e284..0ed67fa18 100644
--- a/client/tokenized-express-checkout/blocks/components/express-checkout-component.js
+++ b/client/tokenized-express-checkout/blocks/components/express-checkout-component.js
@@ -94,7 +94,6 @@ const ExpressCheckoutComponent = ( {
 		onClose,
 		setExpressPaymentError,
 	} );
-	const onClickHandler = ! isPreview ? onButtonClick : () => {};
 	const onShippingAddressChange = ( event ) =>
 		shippingAddressChangeHandler( event, elements );
 
@@ -151,7 +150,7 @@ const ExpressCheckoutComponent = ( {
 	return (
 		<ExpressCheckoutElement
 			options={ checkoutElementOptions }
-			onClick={ onClickHandler }
+			onClick={ onButtonClick }
 			onConfirm={ onConfirm }
 			onReady={ onElementsReady }
 			onCancel={ onCancel }

*/
import { getExpressCheckoutButtonAppearance } from '../../utils';

const ExpressCheckoutButtonPreview = ( {
Copy link
Contributor

Choose a reason for hiding this comment

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

(very optional) since this component contains effects that only apply to the GooglePay button preview, what do you think about separating the previews into two components with their own specific logic?

I'm thinking something like this:

const ApplePayButtonPreview = ( { options, buttonAttributes } ) => {
	// applePay specific logic

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

const GooglePayButtonPreview = ( { options, buttonAttributes } ) => {
	// googlePay specific logic & hooks
	
	useEffect( () => {
		// ...
	}, [] );

	useEffect( () => {
		// ...
	}, [ ] );

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

const ExpressCheckoutButtonPreview = ( {
	expressPaymentMethod,
	options,
	buttonAttributes,
} ) => {
	if ( expressPaymentMethod === 'googlePay' ) {
		return (
			<GooglePayButtonPreview
				options={ options }
				buttonAttributes={ buttonAttributes }
			/>
		);
	}

	if ( expressPaymentMethod === 'applePay' ) {
		return (
			<ApplePayButtonPreview
				options={ options }
				buttonAttributes={ buttonAttributes }
			/>
		);
	}

	return null;
};

export default ExpressCheckoutButtonPreview;

Let me know what you think!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Express Payment button not visibile on the Cart template
3 participants