Skip to content

Commit d7f79a9

Browse files
frossogpressutto5
andauthored
update: tokenized ECE update on price change (#10388)
Co-authored-by: Guilherme Pressutto <[email protected]>
1 parent 7199fbb commit d7f79a9

File tree

3 files changed

+152
-111
lines changed

3 files changed

+152
-111
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: update
3+
4+
update: tokenize ECE initialization and update flow on pricing change.

client/tokenized-express-checkout/compatibility/wc-product-page.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import debounce from '../debounce';
99
* External dependencies
1010
*/
1111
import { addFilter, doAction } from '@wordpress/hooks';
12+
import { getExpressCheckoutData } from 'wcpay/tokenized-express-checkout/utils';
1213

1314
jQuery( ( $ ) => {
1415
$( document.body ).on( 'woocommerce_variation_has_changed', async () => {
@@ -19,6 +20,10 @@ jQuery( ( $ ) => {
1920
// Block the payment request button as soon as an "input" event is fired, to avoid sync issues
2021
// when the customer clicks on the button before the debounced event is processed.
2122
jQuery( ( $ ) => {
23+
if ( getExpressCheckoutData( 'button_context' ) !== 'product' ) {
24+
return;
25+
}
26+
2227
const $quantityInput = $( '.quantity' );
2328
const handleQuantityChange = () => {
2429
expressCheckoutButtonUi.blockButton();

client/tokenized-express-checkout/index.js

Lines changed: 143 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,60 @@ const fetchNewCartData = async () => {
6464
return cartData;
6565
};
6666

67-
const getServerSideExpressCheckoutProductData = () => {
68-
const displayItems = (
69-
getExpressCheckoutData( 'product' )?.displayItems ?? []
70-
).map( ( { label, amount } ) => ( {
71-
name: label,
72-
amount,
73-
} ) );
74-
75-
return {
76-
total: getExpressCheckoutData( 'product' )?.total.amount,
77-
currency: getExpressCheckoutData( 'product' )?.currency,
78-
requestShipping:
79-
getExpressCheckoutData( 'product' )?.needs_shipping ?? false,
80-
requestPhone:
81-
getExpressCheckoutData( 'checkout' )?.needs_payer_phone ?? false,
82-
displayItems,
83-
};
67+
const getTotalAmount = () => {
68+
if ( cachedCartData ) {
69+
return transformPrice(
70+
parseInt( cachedCartData.totals.total_price, 10 ) -
71+
parseInt( cachedCartData.totals.total_refund || 0, 10 ),
72+
cachedCartData.totals
73+
);
74+
}
75+
76+
if (
77+
getExpressCheckoutData( 'button_context' ) === 'product' &&
78+
getExpressCheckoutData( 'product' )
79+
) {
80+
return getExpressCheckoutData( 'product' )?.total.amount;
81+
}
8482
};
8583

84+
const getOnClickOptions = () => {
85+
if ( cachedCartData ) {
86+
return {
87+
// pay-for-order should never display the shipping selection.
88+
shippingAddressRequired:
89+
getExpressCheckoutData( 'button_context' ) !==
90+
'pay_for_order' && cachedCartData.needs_shipping,
91+
shippingRates: transformCartDataForShippingRates( cachedCartData ),
92+
phoneNumberRequired:
93+
getExpressCheckoutData( 'checkout' )?.needs_payer_phone ??
94+
false,
95+
lineItems: transformCartDataForDisplayItems( cachedCartData ),
96+
};
97+
}
98+
99+
if (
100+
getExpressCheckoutData( 'button_context' ) === 'product' &&
101+
getExpressCheckoutData( 'product' )
102+
) {
103+
return {
104+
shippingAddressRequired:
105+
getExpressCheckoutData( 'product' )?.needs_shipping ?? false,
106+
phoneNumberRequired:
107+
getExpressCheckoutData( 'checkout' )?.needs_payer_phone ??
108+
false,
109+
lineItems: (
110+
getExpressCheckoutData( 'product' )?.displayItems ?? []
111+
).map( ( { label, amount } ) => ( {
112+
name: label,
113+
amount,
114+
} ) ),
115+
};
116+
}
117+
};
118+
119+
let elements;
120+
86121
jQuery( ( $ ) => {
87122
// Don't load if blocks checkout is being loaded.
88123
if (
@@ -173,15 +208,16 @@ jQuery( ( $ ) => {
173208
/**
174209
* Starts the Express Checkout Element
175210
*
176-
* @param {Object} options ECE options.
211+
* @param {Object} creationOptions ECE initialization options.
177212
*/
178-
startExpressCheckoutElement: async ( options ) => {
213+
startExpressCheckoutElement: async ( creationOptions ) => {
179214
let addToCartPromise = Promise.resolve();
180215
const stripe = await api.getStripe();
181-
const elements = stripe.elements( {
216+
// https://docs.stripe.com/js/elements_object/create_without_intent
217+
elements = stripe.elements( {
182218
mode: 'payment',
183-
amount: options.total,
184-
currency: options.currency,
219+
amount: creationOptions.total,
220+
currency: creationOptions.currency,
185221
paymentMethodCreation: 'manual',
186222
appearance: getExpressCheckoutButtonAppearance(),
187223
locale: getExpressCheckoutData( 'stripe' )?.locale ?? 'en',
@@ -247,9 +283,12 @@ jQuery( ( $ ) => {
247283
} );
248284
}
249285

286+
const options = getOnClickOptions();
250287
const shippingOptionsWithFallback =
251-
! options.shippingRates || // server-side data on the product page initialization doesn't provide any shipping rates.
252-
options.shippingRates.length === 0 // but it can also happen that there are no rates in the array.
288+
// server-side data on the product page initialization doesn't provide any shipping rates.
289+
! options.shippingRates ||
290+
// but it can also happen that there are no rates in the array.
291+
options.shippingRates.length === 0
253292
? [
254293
// fallback for initialization (and initialization _only_), before an address is provided by the ECE.
255294
{
@@ -263,29 +302,25 @@ jQuery( ( $ ) => {
263302
]
264303
: options.shippingRates;
265304

266-
const clickOptions = {
267-
// `options.displayItems`, `options.requestShipping`, `options.requestPhone`, `options.shippingRates`,
305+
onClickHandler( event );
306+
event.resolve( {
307+
// `options.displayItems`, `options.shippingAddressRequired`, `options.requestPhone`, `options.shippingRates`,
268308
// are all coming from prior of the initialization.
269309
// The "real" values will be updated once the button loads.
270310
// They are preemptively initialized because the `event.resolve({})`
271311
// needs to be called within 1 second of the `click` event.
272312
business: {
273313
name: getExpressCheckoutData( 'store_name' ),
274314
},
275-
lineItems: options.displayItems,
276315
emailRequired: true,
277-
shippingAddressRequired: options.requestShipping,
278-
phoneNumberRequired: options.requestPhone,
279-
shippingRates: options.requestShipping
316+
...options,
317+
shippingRates: options.shippingAddressRequired
280318
? shippingOptionsWithFallback
281319
: undefined,
282320
allowedShippingCountries: getExpressCheckoutData(
283321
'checkout'
284322
).allowed_shipping_countries,
285-
};
286-
287-
onClickHandler( event );
288-
event.resolve( clickOptions );
323+
} );
289324
} );
290325

291326
eceButton.on( 'shippingaddresschange', async ( event ) => {
@@ -333,54 +368,17 @@ jQuery( ( $ ) => {
333368
expressCheckoutButtonUi.getButtonSeparator().show();
334369
}
335370
} );
336-
337-
removeAction(
338-
'wcpay.express-checkout.update-button-data',
339-
'automattic/wcpay/express-checkout'
340-
);
341-
addAction(
342-
'wcpay.express-checkout.update-button-data',
343-
'automattic/wcpay/express-checkout',
344-
async () => {
345-
// if the product cannot be added to cart (because of missing variation selection, etc),
346-
// don't try to add it to the cart to get new data - the call will likely fail.
347-
if (
348-
getExpressCheckoutData( 'button_context' ) === 'product'
349-
) {
350-
const addToCartButton = $(
351-
'.single_add_to_cart_button'
352-
);
353-
354-
// First check if product can be added to cart.
355-
if ( addToCartButton.is( '.disabled' ) ) {
356-
return;
357-
}
358-
}
359-
360-
try {
361-
expressCheckoutButtonUi.blockButton();
362-
363-
cachedCartData = await fetchNewCartData();
364-
365-
// We need to re init the payment request button to ensure the shipping options & taxes are re-fetched.
366-
// The cachedCartData from the Store API will be used from now on,
367-
// instead of the `product` attributes.
368-
wcpayExpressCheckoutParams.product = null;
369-
370-
await wcpayECE.init();
371-
372-
expressCheckoutButtonUi.unblockButton();
373-
} catch ( e ) {
374-
expressCheckoutButtonUi.hideContainer();
375-
}
376-
}
377-
);
378371
},
379372

380373
/**
381374
* Initialize event handlers and UI state
382375
*/
383376
init: async () => {
377+
removeAction(
378+
'wcpay.express-checkout.update-button-data',
379+
'automattic/wcpay/express-checkout'
380+
);
381+
384382
// on product pages, we should be able to have `getExpressCheckoutData( 'product' )` from the backend,
385383
// which saves us some AJAX calls.
386384
if (
@@ -394,9 +392,7 @@ jQuery( ( $ ) => {
394392
if ( ! getExpressCheckoutData( 'product' ) && ! cachedCartData ) {
395393
try {
396394
cachedCartData = await fetchNewCartData();
397-
} catch ( e ) {
398-
// if something fails here, we can likely fall back on `getExpressCheckoutData( 'product' )`.
399-
}
395+
} catch ( e ) {}
400396
}
401397

402398
// once (and if) cart data has been fetched, we can safely clear product data from the backend.
@@ -411,48 +407,84 @@ jQuery( ( $ ) => {
411407
getCartApiHandler().useSeparateCart();
412408
}
413409

414-
if ( cachedCartData ) {
410+
const total = getTotalAmount();
411+
if ( total === 0 ) {
412+
expressCheckoutButtonUi.hideContainer();
413+
expressCheckoutButtonUi.getButtonSeparator().hide();
414+
} else if ( cachedCartData ) {
415415
// If this is the cart page, or checkout page, or pay-for-order page, we need to request the cart details.
416416
// but if the data is not available, we can't render the button.
417-
const total = transformPrice(
418-
parseInt( cachedCartData.totals.total_price, 10 ) -
419-
parseInt( cachedCartData.totals.total_refund || 0, 10 ),
420-
cachedCartData.totals
421-
);
422-
if ( total === 0 ) {
423-
expressCheckoutButtonUi.hideContainer();
424-
expressCheckoutButtonUi.getButtonSeparator().hide();
425-
} else {
426-
await wcpayECE.startExpressCheckoutElement( {
427-
total,
428-
currency: cachedCartData.totals.currency_code.toLowerCase(),
429-
// pay-for-order should never display the shipping selection.
430-
requestShipping:
431-
getExpressCheckoutData( 'button_context' ) !==
432-
'pay_for_order' &&
433-
cachedCartData.needs_shipping,
434-
shippingRates: transformCartDataForShippingRates(
435-
cachedCartData
436-
),
437-
requestPhone:
438-
getExpressCheckoutData( 'checkout' )
439-
?.needs_payer_phone ?? false,
440-
displayItems: transformCartDataForDisplayItems(
441-
cachedCartData
442-
),
443-
} );
444-
}
417+
await wcpayECE.startExpressCheckoutElement( {
418+
total,
419+
currency: cachedCartData.totals.currency_code.toLowerCase(),
420+
} );
445421
} else if (
446422
getExpressCheckoutData( 'button_context' ) === 'product' &&
447423
getExpressCheckoutData( 'product' )
448424
) {
449-
await wcpayECE.startExpressCheckoutElement(
450-
getServerSideExpressCheckoutProductData()
451-
);
425+
await wcpayECE.startExpressCheckoutElement( {
426+
total,
427+
currency: getExpressCheckoutData( 'product' )?.currency,
428+
} );
452429
} else {
453430
expressCheckoutButtonUi.hideContainer();
454431
expressCheckoutButtonUi.getButtonSeparator().hide();
455432
}
433+
434+
addAction(
435+
'wcpay.express-checkout.update-button-data',
436+
'automattic/wcpay/express-checkout',
437+
async () => {
438+
// if the product cannot be added to cart (because of missing variation selection, etc),
439+
// don't try to add it to the cart to get new data - the call will likely fail.
440+
if (
441+
getExpressCheckoutData( 'button_context' ) === 'product'
442+
) {
443+
const addToCartButton = $(
444+
'.single_add_to_cart_button'
445+
);
446+
447+
// First check if product can be added to cart.
448+
if ( addToCartButton.is( '.disabled' ) ) {
449+
return;
450+
}
451+
}
452+
453+
try {
454+
expressCheckoutButtonUi.blockButton();
455+
456+
const prevTotal = getTotalAmount();
457+
458+
cachedCartData = await fetchNewCartData();
459+
460+
// We need to re init the payment request button to ensure the shipping options & taxes are re-fetched.
461+
// The cachedCartData from the Store API will be used from now on,
462+
// instead of the `product` attributes.
463+
wcpayExpressCheckoutParams.product = null;
464+
465+
expressCheckoutButtonUi.unblockButton();
466+
467+
// since the "total" is part of the initialization of the Stripe elements (and not part of the ECE button),
468+
// if the totals change, we might need to update it on the element itself.
469+
const newTotal = getTotalAmount();
470+
if ( ! elements ) {
471+
wcpayECE.init();
472+
} else if ( newTotal !== prevTotal && newTotal > 0 ) {
473+
elements.update( { amount: newTotal } );
474+
}
475+
476+
if ( newTotal === 0 ) {
477+
expressCheckoutButtonUi.hideContainer();
478+
expressCheckoutButtonUi.getButtonSeparator().hide();
479+
} else {
480+
expressCheckoutButtonUi.showContainer();
481+
expressCheckoutButtonUi.getButtonSeparator().show();
482+
}
483+
} catch ( e ) {
484+
expressCheckoutButtonUi.hideContainer();
485+
}
486+
}
487+
);
456488
},
457489
};
458490

0 commit comments

Comments
 (0)