woocommerce/plugins/woocommerce-blocks/assets/js/payment-method-extensions/payment-methods/stripe/payment-request/payment-request-express.js

381 lines
11 KiB
JavaScript
Raw Normal View History

Convert apple pay integration to payment request integration and finish implementation (https://github.com/woocommerce/woocommerce-blocks/pull/2127) * add logic allowing payment method to be overridden via payment data in request * hook in to trigger server side processing of stripe payment request * improvements to shipping data context - memoize event emitters - split up emitted events (reduces how often events trigger) - Include whether rate is being selected in exported data. * expose `isSelectingRate` value to payment method interface * fix typo in shipping emitters for emitter type * include setting of shipping data in payment method success status call - this also requires changing the nested order of providers in checkout provider * fix priority logic for event emitters. - lower priority is supposed to fire before higher priority. * normalize postal code for comparisons * move normalize functions into stripe-utils folder * refactor stripePromise so that it provides a specific instance to each payment method. This also provides it as a prop to the pm components. * renadme apple pay express to payment request express This adds full support for the stripe payment request api instead of just applePay (so GooglePay, MicrosoftPay and ApplePay are now supported). Also adds numerous fixes to internal logic. * add handling to skip core checkout validation logic if express payment method is handling payment Express payment methods have their own internal validation so this removes the need for checkout validating fields. This is also necessary because checkout validation breaks the flow when making a payment using express payment methods because of the order of the flow for these methods. * splitting out emmitter effects for checkout and improving logic Splitting up effects limits the potential for firing off emitters more than needed. * remove unnecessary ref definitions * fix on cancel action erroring for payment request modal * ensure unique stripe object for component and canPay * set default total label if one isn’t configured on the server * fix order of state changes * simplify condition * remove unnecessary dependency * normalize to uppercase too * simplify can make payment conditional * update comment blocks
2020-04-08 16:36:04 +00:00
/**
* Internal dependencies
*/
import {
DEFAULT_STRIPE_EVENT_HANDLERS,
PAYMENT_METHOD_NAME,
} from './constants';
import {
getStripeServerData,
getPaymentRequest,
updatePaymentRequest,
canDoPaymentRequest,
getTotalPaymentItem,
getBillingData,
getPaymentMethodData,
getShippingData,
normalizeShippingAddressForCheckout,
normalizeShippingOptions,
normalizeLineItems,
normalizeShippingOptionSelectionsForCheckout,
} from '../stripe-utils';
/**
* External dependencies
*/
import { useRef, useState, useEffect } from '@wordpress/element';
import {
Elements,
PaymentRequestButtonElement,
useStripe,
} from '@stripe/react-stripe-js';
import { __ } from '@wordpress/i18n';
/**
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
* @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').RegisteredPaymentMethodProps} RegisteredPaymentMethodProps
*/
/**
* @typedef {Object} WithStripe
*
* @property {Stripe} [stripe] Stripe api (might not be present)
*/
/**
* @typedef {RegisteredPaymentMethodProps & WithStripe} StripeRegisteredPaymentMethodProps
*/
/**
* PaymentRequestExpressComponent
*
* @param {StripeRegisteredPaymentMethodProps} props Incoming props
*/
const PaymentRequestExpressComponent = ( {
shippingData,
billing,
eventRegistration,
onSubmit,
activePaymentMethod,
setActivePaymentMethod,
setExpressPaymentError,
} ) => {
/**
* @type {[ StripePaymentRequest|null, function( StripePaymentRequest ):StripePaymentRequest|null]}
*/
// @ts-ignore
const [ paymentRequest, setPaymentRequest ] = useState( null );
const stripe = useStripe();
const [ canMakePayment, setCanMakePayment ] = useState( false );
const [ paymentRequestType, setPaymentRequestType ] = useState( '' );
const [ isProcessing, setIsProcessing ] = useState( false );
const [ isFinished, setIsFinished ] = useState( false );
const eventHandlers = useRef( DEFAULT_STRIPE_EVENT_HANDLERS );
const currentBilling = useRef( billing );
const currentShipping = useRef( shippingData );
const currentPaymentRequest = useRef( paymentRequest );
// for keeping track of what the active payment method was before this
// payment button was clicked.
const originalActivePaymentMethod = useRef( activePaymentMethod );
// update refs when any change.
useEffect( () => {
currentBilling.current = billing;
currentShipping.current = shippingData;
currentPaymentRequest.current = paymentRequest;
}, [ billing, shippingData, paymentRequest ] );
// set paymentRequest.
useEffect( () => {
// can't do anything if stripe isn't available yet or we have zero total.
if ( ! stripe || ! billing.cartTotal.value ) {
return;
}
// if payment request hasn't been set yet then set it.
if ( ! currentPaymentRequest.current && ! isFinished ) {
setPaymentRequest(
getPaymentRequest( {
total: billing.cartTotal,
currencyCode: billing.currency.code.toLowerCase(),
countryCode: shippingData.shippingAddress.country,
shippingRequired: shippingData.needsShipping,
cartTotalItems: billing.cartTotalItems,
stripe,
} )
);
}
// otherwise we just update it (but only if payment processing hasn't
// already started).
if ( ! isProcessing && currentPaymentRequest.current && ! isFinished ) {
updatePaymentRequest( {
// @ts-ignore
paymentRequest: currentPaymentRequest.current,
total: billing.cartTotal,
currencyCode: billing.currency.code.toLowerCase(),
cartTotalItems: billing.cartTotalItems,
} );
}
}, [
billing.cartTotal,
billing.currency.code,
shippingData.shippingAddress.country,
shippingData.needsShipping,
billing.cartTotalItems,
stripe,
isProcessing,
isFinished,
] );
// whenever paymentRequest changes, then we need to update whether
// payment can be made.
useEffect( () => {
if ( paymentRequest ) {
canDoPaymentRequest( paymentRequest ).then( ( result ) => {
if ( result.requestType ) {
setPaymentRequestType( result.requestType );
}
setCanMakePayment( result.canPay );
} );
}
}, [ paymentRequest ] );
// kick off payment processing.
const onButtonClick = () => {
originalActivePaymentMethod.current = activePaymentMethod;
setActivePaymentMethod( PAYMENT_METHOD_NAME );
setIsProcessing( true );
setIsFinished( false );
setExpressPaymentError( '' );
};
const abortPayment = ( paymentMethod, message ) => {
const response = {
fail: {
message,
billingData: getBillingData( paymentMethod ),
paymentMethodData: getPaymentMethodData(
paymentMethod,
paymentRequestType
),
},
};
paymentMethod.complete( 'fail' );
setIsProcessing( false );
setIsFinished( true );
return response;
};
const completePayment = ( paymentMethod ) => {
paymentMethod.complete( 'success' );
setIsFinished( true );
setIsProcessing( false );
};
// event callbacks.
const onShippingRatesEvent = ( forSuccess = true ) => ( shippingRates ) => {
const handlers = eventHandlers.current;
const billingData = currentBilling.current;
if ( handlers.shippingAddressChange && isProcessing ) {
handlers.shippingAddressChange.updateWith( {
status: forSuccess ? 'success' : 'fail',
shippingOptions: normalizeShippingOptions( shippingRates ),
total: getTotalPaymentItem( billingData.cartTotal ),
displayItems: normalizeLineItems( billingData.cartTotalItems ),
} );
handlers.shippingAddressChange = null;
}
};
const onShippingSelectedRate = ( forSuccess = true ) => () => {
const handlers = eventHandlers.current;
const shipping = currentShipping.current;
const billingData = currentBilling.current;
if (
handlers.shippingOptionChange &&
! shipping.isSelectingRate &&
isProcessing
) {
const updateObject = forSuccess
? {
status: 'success',
total: getTotalPaymentItem( billingData.cartTotal ),
displayItems: normalizeLineItems(
billingData.cartTotalItems
),
}
: {
status: 'fail',
};
handlers.shippingOptionChange.updateWith( updateObject );
handlers.shippingOptionChange = null;
}
};
const onPaymentProcessing = () => {
const handlers = eventHandlers.current;
if ( handlers.sourceEvent && isProcessing ) {
const response = {
billingData: getBillingData( handlers.sourceEvent ),
paymentMethodData: getPaymentMethodData(
handlers.sourceEvent,
paymentRequestType
),
shippingData: getShippingData( handlers.sourceEvent ),
};
return response;
}
return true;
};
const onCheckoutComplete = ( forSuccess = true ) => () => {
const handlers = eventHandlers.current;
if ( handlers.sourceEvent && isProcessing ) {
if ( forSuccess ) {
completePayment( handlers.sourceEvent );
} else {
abortPayment( handlers.sourceEvent );
}
handlers.sourceEvent = null;
}
return true;
};
// when canMakePayment is true, then we set listeners on payment request for
// handling updates.
useEffect( () => {
if ( paymentRequest && canMakePayment && isProcessing ) {
paymentRequest.on( 'shippingaddresschange', ( event ) => {
// @todo check if there is an address change, and if not, then
// just call updateWith and don't call setShippingAddress here
// because the state won't change upstream.
currentShipping.current.setShippingAddress(
normalizeShippingAddressForCheckout( event.shippingAddress )
);
eventHandlers.current.shippingAddressChange = event;
} );
paymentRequest.on( 'shippingoptionchange', ( event ) => {
currentShipping.current.setSelectedRates(
normalizeShippingOptionSelectionsForCheckout(
event.shippingOption
)
);
eventHandlers.current.shippingOptionChange = event;
} );
paymentRequest.on( 'source', ( paymentMethod ) => {
if (
// eslint-disable-next-line no-undef
! getStripeServerData().allowPrepaidCard &&
paymentMethod.source.card.funding
) {
setExpressPaymentError(
__(
"Sorry, we're not accepting prepaid cards at this time.",
'woocommerce-gateway-stripe'
)
);
return;
}
eventHandlers.current.sourceEvent = paymentMethod;
// kick off checkout processing step.
onSubmit();
} );
paymentRequest.on( 'cancel', () => {
setIsFinished( true );
setIsProcessing( false );
setActivePaymentMethod( originalActivePaymentMethod.current );
} );
}
}, [
paymentRequest,
canMakePayment,
isProcessing,
setActivePaymentMethod,
] );
// subscribe to events.
useEffect( () => {
if ( canMakePayment && isProcessing ) {
const subscriber = eventRegistration;
const unsubscribeShippingRateSuccess = subscriber.onShippingRateSuccess(
onShippingRatesEvent()
);
const unsubscribeShippingRateFail = subscriber.onShippingRateFail(
onShippingRatesEvent( false )
);
const unsubscribeShippingRateSelectSuccess = subscriber.onShippingRateSelectSuccess(
onShippingSelectedRate()
);
const unsubscribeShippingRateSelectFail = subscriber.onShippingRateSelectFail(
onShippingRatesEvent( false )
);
const unsubscribePaymentProcessing = subscriber.onPaymentProcessing(
onPaymentProcessing
);
const unsubscribeCheckoutCompleteSuccess = subscriber.onCheckoutCompleteSuccess(
onCheckoutComplete()
);
const unsubscribeCheckoutCompleteFail = subscriber.onCheckoutCompleteError(
onCheckoutComplete( false )
);
return () => {
unsubscribeCheckoutCompleteFail();
unsubscribeCheckoutCompleteSuccess();
unsubscribePaymentProcessing();
unsubscribeShippingRateFail();
unsubscribeShippingRateSuccess();
unsubscribeShippingRateSelectSuccess();
unsubscribeShippingRateSelectFail();
};
}
return undefined;
}, [
canMakePayment,
isProcessing,
eventRegistration.onShippingRateSuccess,
eventRegistration.onShippingRateFail,
eventRegistration.onShippingRateSelectSuccess,
eventRegistration.onShippingRateSelectFail,
eventRegistration.onPaymentProcessing,
eventRegistration.onCheckoutCompleteSuccess,
eventRegistration.onCheckoutCompleteError,
] );
// locale is not a valid value for the paymentRequestButton style.
const { theme } = getStripeServerData().button;
const paymentRequestButtonStyle = {
paymentRequestButton: {
type: 'default',
theme,
height: '48px',
},
};
return canMakePayment && paymentRequest ? (
<PaymentRequestButtonElement
onClick={ onButtonClick }
options={ {
style: paymentRequestButtonStyle,
paymentRequest,
} }
/>
) : null;
};
/**
* PaymentRequestExpress with stripe provider
*
* @param {StripeRegisteredPaymentMethodProps} props
*/
export const PaymentRequestExpress = ( props ) => {
const { locale } = getStripeServerData().button;
const { stripe } = props;
return (
<Elements stripe={ stripe } locale={ locale }>
<PaymentRequestExpressComponent { ...props } />
</Elements>
);
};