Skip to content

Commit

Permalink
Implement payments via Express Checkout Element for Simple Products o…
Browse files Browse the repository at this point in the history
…n the Product Page (#8937)

Co-authored-by: Rafael Zaleski <rafaelzaleski@users.noreply.github.com>
Co-authored-by: Kristófer R <kristofer.thorlaksson@automattic.com>
Co-authored-by: Kristófer Reykjalín <13835680+reykjalin@users.noreply.github.com>
Co-authored-by: César Costa <10233985+cesarcosta99@users.noreply.github.com>
  • Loading branch information
5 people committed Jun 18, 2024
1 parent fa75540 commit a66ec0b
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 68 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"@typescript-eslint/no-explicit-any": "off",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [ "error" ],
"@typescript-eslint/explicit-module-boundary-types": "off",
"wpcalypso/jsx-classname-namespace": "off",
"react/react-in-jsx-scope": "error",
"no-shadow": "off",
Expand Down
4 changes: 4 additions & 0 deletions changelog/add-8870-ece-for-shortcode-cart-and-checkout
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add support for Express Checkout Element on shortcode Cart and Checkout pages.
4 changes: 4 additions & 0 deletions changelog/as-8868-ece-product-page
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add support for the Express Checkout Element on product pages.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* External dependencies
*/
import { ExpressCheckoutElement } from '@stripe/react-stripe-js';
import { shippingAddressChangeHandler } from '../../event-handlers';
import {
shippingAddressChangeHandler,
shippingRateChangeHandler,
} from '../../event-handlers';
import { useExpressCheckout } from '../hooks/use-express-checkout';

/**
Expand All @@ -25,6 +28,7 @@ const ExpressCheckoutComponent = ( {
onButtonClick,
onConfirm,
onCancel,
elements,
} = useExpressCheckout( {
api,
billing,
Expand All @@ -34,9 +38,11 @@ const ExpressCheckoutComponent = ( {
setExpressPaymentError,
} );

const onShippingAddressChange = ( event ) => {
shippingAddressChangeHandler( api, event );
};
const onShippingAddressChange = ( event ) =>
shippingAddressChangeHandler( api, event, elements );

const onShippingRateChange = ( event ) =>
shippingRateChangeHandler( api, event, elements );

return (
<ExpressCheckoutElement
Expand All @@ -45,6 +51,7 @@ const ExpressCheckoutComponent = ( {
onConfirm={ onConfirm }
onCancel={ onCancel }
onShippingAddressChange={ onShippingAddressChange }
onShippingRateChange={ onShippingRateChange }
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,6 @@ export const useExpressCheckout = ( {
onButtonClick,
onConfirm,
onCancel,
elements,
};
};
52 changes: 42 additions & 10 deletions client/express-checkout/event-handlers.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
/**
* Internal dependencies
*/
import { normalizeOrderData, normalizeShippingAddress } from './utils';
import { getErrorMessageFromNotice } from 'utils/express-checkout';
import {
normalizeOrderData,
normalizeShippingAddress,
normalizeLineItems,
} from './utils';
import { getErrorMessageFromNotice } from './utils/index';

export const shippingAddressChangeHandler = async ( api, event ) => {
export const shippingAddressChangeHandler = async ( api, event, elements ) => {
const response = await api.expressCheckoutECECalculateShippingOptions(
normalizeShippingAddress( event.shippingAddress )
normalizeShippingAddress( event.address )
);
event.resolve( {
shippingRates: response.shipping_options,
} );

if ( response.result === 'success' ) {
elements.update( {
amount: response.total.amount,
} );
event.resolve( {
shippingRates: response.shipping_options,
lineItems: normalizeLineItems( response.displayItems ),
} );
} else {
event.reject();
}
};

export const shippingRateChangeHandler = async ( api, event, elements ) => {
const response = await api.paymentRequestUpdateShippingDetails(
event.shippingRate
);

if ( response.result === 'success' ) {
elements.update( { amount: response.total.amount } );
event.resolve( {
lineItems: normalizeLineItems( response.displayItems ),
} );
} else {
event.reject();
}
};

export const onConfirmHandler = async (
Expand All @@ -21,13 +49,17 @@ export const onConfirmHandler = async (
abortPayment,
event
) => {
const { error: submitError } = await elements.submit();
if ( submitError ) {
return abortPayment( event, submitError.message );
}

const { paymentMethod, error } = await stripe.createPaymentMethod( {
elements,
} );

if ( error ) {
abortPayment( event, error.message );
return;
return abortPayment( event, error.message );
}

// Kick off checkout processing step.
Expand Down Expand Up @@ -56,6 +88,6 @@ export const onConfirmHandler = async (
completePayment( redirectUrl );
}
} catch ( e ) {
abortPayment( event, error.message );
return abortPayment( event, error.message );
}
};
149 changes: 107 additions & 42 deletions client/express-checkout/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/* global jQuery, wcpayExpressCheckoutParams */
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import WCPayAPI from '../checkout/api';
import '../checkout/express-checkout-buttons.scss';
import { getExpressCheckoutData, normalizeLineItems } from './utils/index';
import {
onConfirmHandler,
shippingAddressChangeHandler,
shippingRateChangeHandler,
} from './event-handlers';

jQuery( ( $ ) => {
// Don't load if blocks checkout is being loaded.
Expand Down Expand Up @@ -40,11 +47,6 @@ jQuery( ( $ ) => {
* Object to handle Stripe payment forms.
*/
const wcpayECE = {
/**
* Whether the payment was aborted by the customer.
*/
paymentAborted: false,

getAttributes: function () {
const select = $( '.variations_form' ).find( '.variations select' );
const data = {};
Expand Down Expand Up @@ -73,13 +75,14 @@ jQuery( ( $ ) => {
},

/**
* Abort payment and display error messages.
* Abort the payment and display error messages.
*
* @param {PaymentResponse} payment Payment response instance.
* @param {string} message Error message to display.
* @param {string} message Error message to display.
*/
abortPayment: ( payment, message ) => {
payment.complete( 'fail' );
payment.paymentFailed();
wcpayECE.unblock();

$( '.woocommerce-error' ).remove();

Expand Down Expand Up @@ -121,6 +124,10 @@ jQuery( ( $ ) => {
} );
},

unblock: () => {
$.unblockUI();
},

/**
* Adds the item to the cart and return cart details.
*
Expand Down Expand Up @@ -177,51 +184,104 @@ jQuery( ( $ ) => {
* @param {Object} options ECE options.
*/
startExpressCheckoutElement: ( options ) => {
const getShippingRates = () => {
if ( ! options.requestShipping ) {
return [];
}

if ( getExpressCheckoutData( 'is_product_page' ) ) {
// Despite the name of the property, this seems to be just a single option that's not in an array.
const {
shippingOptions: shippingOption,
} = getExpressCheckoutData( 'product' );

return [
{
id: shippingOption.id,
amount: shippingOption.amount,
displayName: shippingOption.label,
},
];
}

return options.displayItems
.filter(
( i ) =>
i.label === __( 'Shipping', 'woocommerce-payments' )
)
.map( ( i ) => ( {
id: `rate-${ i.label }`,
amount: i.amount,
displayName: i.label,
} ) );
};

const shippingRates = getShippingRates();

// This is a bit of a hack, but we need some way to get the shipping information before rendering the button, and
// since we don't have any address information at this point it seems best to rely on what came with the cart response.
// Relying on what's provided in the cart response seems safest since it should always include a valid shipping
// rate if one is required and available.
// If no shipping rate is found we can't render the button so we just exit.
if ( options.requestShipping && ! shippingRates ) {
return;
}

const elements = api.getStripe().elements( {
mode: options?.mode ?? 'payment',
amount: options?.total,
currency: options?.currency,
paymentMethodCreation: 'manual',
} );

const eceButton = wcpayECE.createButton( elements, {
buttonType: {
googlePay: wcpayExpressCheckoutParams.button.type,
applePay: wcpayExpressCheckoutParams.button.type,
googlePay: getExpressCheckoutData( 'button' ).type,
applePay: getExpressCheckoutData( 'button' ).type,
},
} );

wcpayECE.showButton( eceButton );

wcpayECE.attachButtonEventListeners( eceButton );

eceButton.on( 'click', function ( event ) {
// TODO: handle cases where we need login confirmation.

if ( getExpressCheckoutData( 'is_product_page' ) ) {
wcpayECE.addToCart();
}

const clickOptions = {
business: {
name: 'Mikes Bikes',
},
lineItems: [
{ name: 'Bike', amount: 200 },
{ name: 'Helmet', amount: 300 },
],
shippingAddressRequired: true,
shippingRates: [
{
id: '1',
amount: 500,
displayName: 'Standard Shipping',
},
{
id: '2',
amount: 1000,
displayName: 'Expedited Shipping',
},
],
lineItems: normalizeLineItems( options.displayItems ),
emailRequired: true,
shippingAddressRequired: options.requestShipping,
phoneNumberRequired: options.requestPhone,
shippingRates,
};
wcpayECE.block();
event.resolve( clickOptions );
} );

eceButton.on( 'cancel', () => {
wcpayECE.paymentAborted = true;
eceButton.on( 'shippingaddresschange', async ( event ) =>
shippingAddressChangeHandler( api, event, elements )
);

eceButton.on( 'shippingratechange', async ( event ) =>
shippingRateChangeHandler( api, event, elements )
);

eceButton.on( 'confirm', async ( event ) =>
onConfirmHandler(
api,
api.getStripe(),
elements,
wcpayECE.completePayment,
wcpayECE.abortPayment,
event
)
);

eceButton.on( 'cancel', async () => {
wcpayECE.unblock();
} );
},

Expand Down Expand Up @@ -337,10 +397,14 @@ jQuery( ( $ ) => {
} else if ( wcpayExpressCheckoutParams.is_product_page ) {
wcpayECE.startExpressCheckoutElement( {
mode: 'payment',
total: wcpayExpressCheckoutParams.product.total.amount,
currency: 'usd',
total: getExpressCheckoutData( 'product' )?.total.amount,
currency: getExpressCheckoutData( 'product' )?.currency,
requestShipping:
wcpayExpressCheckoutParams.product.needs_shipping,
getExpressCheckoutData( 'product' )?.needs_shipping ??
false,
requestPhone:
getExpressCheckoutData( 'checkout' )
?.needs_payer_phone ?? false,
displayItems:
wcpayExpressCheckoutParams.product.displayItems,
} );
Expand All @@ -350,16 +414,17 @@ jQuery( ( $ ) => {
api.paymentRequestGetCartDetails().then( ( cart ) => {
wcpayECE.startExpressCheckoutElement( {
mode: 'payment',
total: 1000,
currency: 'usd',
total: cart.total.amount,
currency: getExpressCheckoutData( 'checkout' )
?.currency_code,
requestShipping: cart.needs_shipping,
requestPhone:
getExpressCheckoutData( 'checkout' )
?.needs_payer_phone ?? false,
displayItems: cart.displayItems,
} );
} );
}

// After initializing a new element, we need to reset the paymentAborted flag.
wcpayECE.paymentAborted = false;
},
};

Expand Down
1 change: 0 additions & 1 deletion client/express-checkout/utils/index.js

This file was deleted.

Loading

0 comments on commit a66ec0b

Please sign in to comment.