Skip to content

Commit 2ae0cff

Browse files
committed
ECE: fix cart timeout issues (#3862)
* Clear the cart before proceeding with express checkout * Add timeout to add-to-cart operation, and resolve click event on timeout.
1 parent a3307ef commit 2ae0cff

File tree

3 files changed

+140
-57
lines changed

3 files changed

+140
-57
lines changed

changelog.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
= 9.2.0 - xxxx-xx-xx =
44
* Dev - Replaces part of the StoreAPI call code for the cart endpoints to use the newly introduced filter.
5+
* Fix - Clear cart first when using express checkout inside the product page.
6+
* Fix - Avoid Stripe timeouts for the express checkout click event.
57
* Fix - Switch booking products back to using non-StoreAPI add-to-cart methods.
68
* Dev - Add new E2E tests for Link express checkout.
79
* Add - Add Amazon Pay to block cart and block checkout.

client/entrypoints/express-checkout/index.js

Lines changed: 136 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,100 @@ jQuery( function ( $ ) {
8787
$( '.variations_form' ).length > 0 ||
8888
$( '.wc-bookings-booking-form' ).length > 0;
8989

90+
const resolveClickEvent = ( event, options ) => {
91+
const clickOptions = {
92+
lineItems: useLegacyCartEndpoints
93+
? normalizeLineItems( options.displayItems )
94+
: options.displayItems,
95+
emailRequired: true,
96+
shippingAddressRequired: options.requestShipping,
97+
phoneNumberRequired: options.requestPhone,
98+
shippingRates: options.shippingRates,
99+
};
100+
101+
return event.resolve( clickOptions );
102+
};
103+
104+
const handleProductPageECEButtonClick = async ( event, options ) => {
105+
const addToCartButton = document.querySelector(
106+
'.single_add_to_cart_button'
107+
);
108+
109+
// First check if product can be added to cart.
110+
if ( addToCartButton.classList.contains( 'disabled' ) ) {
111+
const defaultMessage = __(
112+
'Please select your product options before proceeding.',
113+
'woocommerce-gateway-stripe'
114+
);
115+
let message;
116+
if (
117+
addToCartButton.classList.contains(
118+
'wc-variation-is-unavailable'
119+
)
120+
) {
121+
message =
122+
getAddToCartVariationParams( 'i18n_unavailable_text' ) ||
123+
__(
124+
'Sorry, this product is unavailable. Please choose a different combination.',
125+
'woocommerce-gateway-stripe'
126+
);
127+
}
128+
129+
// eslint-disable-next-line no-alert
130+
window.alert( message || defaultMessage );
131+
return;
132+
}
133+
134+
if ( wcStripeECEError ) {
135+
// eslint-disable-next-line no-alert
136+
window.alert( wcStripeECEError );
137+
return;
138+
}
139+
140+
// Stripe requires event.resolve() to be called within 1s of the click event.
141+
// Here, we enforce a timeout for the addToCart operation. If the operation
142+
// takes longer, we will call event.resolve() immediately,
143+
// and wait for the addToCart operation to finish after.
144+
const addToCartPromise = wcStripeECE.addToCart();
145+
const timeout = new Promise( ( resolve ) =>
146+
setTimeout( () => {
147+
resolve( 'timeout' );
148+
}, 700 )
149+
);
150+
const result = await Promise.race( [ addToCartPromise, timeout ] );
151+
if ( result === 'timeout' ) {
152+
// Immediately resolve the click event to avoid the 1s timeout.
153+
resolveClickEvent( event, options );
154+
155+
// Wait for the addToCart operation to finish, checking
156+
// that the product was successfully added to the cart.
157+
wcStripeECE.isAddToCartSuccessful = false;
158+
const response = await addToCartPromise;
159+
const isAddToCartSuccessful = response?.items_count > 0;
160+
const isLegacyAddToCartSuccessful = response?.result === 'success';
161+
if ( isAddToCartSuccessful || isLegacyAddToCartSuccessful ) {
162+
wcStripeECE.isAddToCartSuccessful = true;
163+
}
164+
165+
return;
166+
}
167+
168+
wcStripeECE.isAddToCartSuccessful = true;
169+
return resolveClickEvent( event, options );
170+
};
171+
172+
const handleProductPageShippingAddressChange = async (
173+
event,
174+
elements
175+
) => {
176+
if ( wcStripeECE.isAddToCartSuccessful === false ) {
177+
// wait 1s for the item to be added to the cart before proceeding
178+
await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );
179+
}
180+
181+
return shippingAddressChangeHandler( api, event, elements );
182+
};
183+
90184
const wcStripeECE = {
91185
createButton: ( elements, options ) =>
92186
elements.create( 'expressCheckout', options ),
@@ -254,66 +348,27 @@ jQuery( function ( $ ) {
254348
);
255349
}
256350

257-
if ( getExpressCheckoutData( 'is_product_page' ) ) {
258-
const addToCartButton = $( '.single_add_to_cart_button' );
259-
260-
// First check if product can be added to cart.
261-
if ( addToCartButton.is( '.disabled' ) ) {
262-
if (
263-
addToCartButton.is( '.wc-variation-is-unavailable' )
264-
) {
265-
// eslint-disable-next-line no-alert
266-
window.alert(
267-
// eslint-disable-next-line camelcase
268-
getAddToCartVariationParams(
269-
'i18n_unavailable_text'
270-
) ||
271-
__(
272-
'Sorry, this product is unavailable. Please choose a different combination.',
273-
'woocommerce-gateway-stripe'
274-
)
275-
);
276-
} else {
277-
// eslint-disable-next-line no-alert
278-
window.alert(
279-
__(
280-
'Please select your product options before proceeding.',
281-
'woocommerce-gateway-stripe'
282-
)
283-
);
284-
}
285-
return;
286-
}
287-
288-
if ( wcStripeECEError ) {
289-
// eslint-disable-next-line no-alert
290-
window.alert( wcStripeECEError );
291-
return;
292-
}
293-
294-
// Add products to the cart if everything is right.
295-
await wcStripeECE.addToCart();
351+
if ( ! getExpressCheckoutData( 'is_product_page' ) ) {
352+
onClickHandler( event );
353+
return resolveClickEvent( event, options );
296354
}
297355

298-
const clickOptions = {
299-
lineItems: useLegacyCartEndpoints
300-
? normalizeLineItems( options.displayItems )
301-
: options.displayItems,
302-
emailRequired: true,
303-
shippingAddressRequired: options.requestShipping,
304-
phoneNumberRequired: options.requestPhone,
305-
shippingRates: options.shippingRates,
306-
};
307-
308-
onClickHandler( event );
309-
event.resolve( clickOptions );
356+
return await handleProductPageECEButtonClick( event, options );
310357
} );
311358

312-
eceButton.on(
313-
'shippingaddresschange',
314-
async ( event ) =>
315-
await shippingAddressChangeHandler( api, event, elements )
316-
);
359+
eceButton.on( 'shippingaddresschange', async ( event ) => {
360+
if ( getExpressCheckoutData( 'is_product_page' ) ) {
361+
return await handleProductPageShippingAddressChange(
362+
event,
363+
elements
364+
);
365+
}
366+
return await shippingAddressChangeHandler(
367+
api,
368+
event,
369+
elements
370+
);
371+
} );
317372

318373
eceButton.on(
319374
'shippingratechange',
@@ -322,6 +377,24 @@ jQuery( function ( $ ) {
322377
);
323378

324379
eceButton.on( 'confirm', async ( event ) => {
380+
if (
381+
getExpressCheckoutData( 'is_product_page' ) &&
382+
wcStripeECE.isAddToCartSuccessful === false
383+
) {
384+
// wait 1s for the item to be added to the cart before proceeding
385+
await new Promise( ( resolve ) =>
386+
setTimeout( resolve, 1000 )
387+
);
388+
389+
if ( wcStripeECE.isAddToCartSuccessful === false ) {
390+
const message = __(
391+
'There was an error adding the product to the cart.',
392+
'woocommerce-gateway-stripe'
393+
);
394+
return wcStripeECE.abortPayment( event, message );
395+
}
396+
}
397+
325398
const order = options.order ? options.order : 0;
326399
const orderDetails = options.orderDetails ?? {};
327400
return await onConfirmHandler( {
@@ -530,7 +603,7 @@ jQuery( function ( $ ) {
530603
*
531604
* @return {Promise} Promise for the request to the server.
532605
*/
533-
addToCart: () => {
606+
addToCart: async () => {
534607
let productId = $( '.single_add_to_cart_button' ).val();
535608

536609
const data = {
@@ -573,6 +646,12 @@ jQuery( function ( $ ) {
573646
data.id = productId;
574647
data.variation = [];
575648

649+
// Clear the cart, so items that are currently in it
650+
// do not interfere with computed totals.
651+
// Use the non-StoreAPI method as it is faster; Stripe requires
652+
// the click event to be resolved within 1 second.
653+
await api.expressCheckoutEmptyCartLegacy( {} );
654+
576655
return api.expressCheckoutAddToCart( data );
577656
},
578657

readme.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
112112

113113
= 9.2.0 - xxxx-xx-xx =
114114
* Dev - Replaces part of the StoreAPI call code for the cart endpoints to use the newly introduced filter.
115+
* Fix - Clear cart first when using express checkout inside the product page.
116+
* Fix - Avoid Stripe timeouts for the express checkout click event.
115117
* Fix - Switch booking products back to using non-StoreAPI add-to-cart methods.
116118
* Dev - Add new E2E tests for Link express checkout.
117119
* Add - Add Amazon Pay to block cart and block checkout.

0 commit comments

Comments
 (0)