@@ -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+
86121jQuery ( ( $ ) => {
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