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

261 lines
7.1 KiB
JavaScript
Raw Normal View History

Fix shipping rate and address handling in Stripe payment request payment method. (https://github.com/woocommerce/woocommerce-blocks/pull/2484) * fix dependencies * refactor stripe payment-request to extract things into smaller units - adds/fixes typedefs - fixes dependencies - improves logic. * implement memoizing for functions. * if same shipping address is selected, just call updateWith immediately * add separate handler for failed shipping rate retrieval * improve logic around shipping rate fail/success status * add notice suppression logic to store notices. - this is implemented in checkout processor to suppress notices when express payment methods are active. * add error detection for shipping address errors and update the shipping status accordingly * update type-def * set billingData before shippingData This is needed because of the shipping data and billing data sync logic in use-checkout-address. * have to tighten dependencies to prevent unnecessary firing With us now adding error status setters for shippping, the potential for the shipping status changes to trigger the effect went up. So tightening the dependencies to only the stati we care about prevent unnecessary effect calls. * refactor event handlers to be named and remove all listeners. This is an undocumented api on the stripe `paymentRequest.on` return value, but I’m trusting it will be relatively stable for this api. The need for this is caused by the fact that without it, the listeners are re-registered on the paymentRequest event everytime the paymentRequest modal is closed and reopened. * fix typo in doc block
2020-05-14 23:55:22 +00:00
/**
* External dependencies
*/
import { useEffect, useState, useRef, useCallback } from '@wordpress/element';
import { useStripe } from '@stripe/react-stripe-js';
import { getSetting } from '@woocommerce/settings';
import { __ } from '@wordpress/i18n';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* Internal dependencies
*/
import {
getPaymentRequest,
updatePaymentRequest,
canDoPaymentRequest,
getBillingData,
getPaymentMethodData,
normalizeShippingAddressForCheckout,
normalizeShippingOptionSelectionsForCheckout,
getStripeServerData,
pluckAddress,
normalizeShippingOptions,
} from '../stripe-utils';
import { useEventHandlers } from './use-event-handlers';
/**
* @typedef {import('../stripe-utils/type-defs').StripePaymentRequest} StripePaymentRequest
*/
export const useInitialization = ( {
billing,
shippingData,
setExpressPaymentError,
onClick,
onClose,
onSubmit,
} ) => {
const stripe = useStripe();
/**
* @type {[ StripePaymentRequest|null, function( StripePaymentRequest ):void]}
*/
// @ts-ignore
const [ paymentRequest, setPaymentRequest ] = useState( null );
const [ isFinished, setIsFinished ] = useState( false );
const [ isProcessing, setIsProcessing ] = useState( false );
const [ canMakePayment, setCanMakePayment ] = useState( false );
const currentPaymentRequest = useRef( paymentRequest );
const currentPaymentRequestType = useRef( '' );
const currentShipping = useRef( shippingData );
const {
paymentRequestEventHandlers,
clearPaymentRequestEventHandler,
setPaymentRequestEventHandler,
} = useEventHandlers();
// Update refs when any change.
useEffect( () => {
currentPaymentRequest.current = paymentRequest;
currentShipping.current = shippingData;
}, [ paymentRequest, shippingData ] );
// 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: getSetting( 'baseLocation', {} )?.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.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 ) {
currentPaymentRequestType.current = result.requestType;
}
setCanMakePayment( result.canPay );
} );
}
}, [ paymentRequest ] );
// kick off payment processing.
const onButtonClick = () => {
setIsProcessing( true );
setIsFinished( false );
setExpressPaymentError( '' );
onClick();
};
const abortPayment = useCallback( ( paymentMethod, message ) => {
const response = {
fail: {
message,
billingData: getBillingData( paymentMethod ),
paymentMethodData: getPaymentMethodData(
paymentMethod,
currentPaymentRequestType.current
),
},
};
paymentMethod.complete( 'fail' );
setIsProcessing( false );
setIsFinished( true );
return response;
}, [] );
const completePayment = useCallback( ( paymentMethod ) => {
paymentMethod.complete( 'success' );
setIsFinished( true );
setIsProcessing( false );
}, [] );
// when canMakePayment is true, then we set listeners on payment request for
// handling updates.
useEffect( () => {
const shippingAddressChangeHandler = ( event ) => {
const newShippingAddress = normalizeShippingAddressForCheckout(
event.shippingAddress
);
if (
isShallowEqual(
pluckAddress( newShippingAddress ),
pluckAddress( currentShipping.current.shippingAddress )
)
) {
// the address is the same so no change needed.
event.updateWith( {
status: 'success',
shippingOptions: normalizeShippingOptions(
currentShipping.current.shippingRates
),
} );
} else {
// the address is different so let's set the new address and
// register the handler to be picked up by the shipping rate
// change event.
currentShipping.current.setShippingAddress(
normalizeShippingAddressForCheckout( event.shippingAddress )
);
setPaymentRequestEventHandler( 'shippingAddressChange', event );
}
};
const shippingOptionChangeHandler = ( event ) => {
currentShipping.current.setSelectedRates(
normalizeShippingOptionSelectionsForCheckout(
event.shippingOption
)
);
setPaymentRequestEventHandler( 'shippingOptionChange', event );
};
const sourceHandler = ( 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;
}
setPaymentRequestEventHandler( 'sourceEvent', paymentMethod );
// kick off checkout processing step.
onSubmit();
};
const cancelHandler = () => {
setIsFinished( true );
setIsProcessing( false );
onClose();
};
const noop = { removeAllListeners: () => void null };
let shippingAddressChangeEvent = noop,
shippingOptionChangeEvent = noop,
sourceChangeEvent = noop,
cancelChangeEvent = noop;
if ( paymentRequest && canMakePayment && isProcessing ) {
// @ts-ignore
shippingAddressChangeEvent = paymentRequest.on(
'shippingaddresschange',
shippingAddressChangeHandler
);
// @ts-ignore
shippingOptionChangeEvent = paymentRequest.on(
'shippingoptionchange',
shippingOptionChangeHandler
);
// @ts-ignore
sourceChangeEvent = paymentRequest.on( 'source', sourceHandler );
// @ts-ignore
cancelChangeEvent = paymentRequest.on( 'cancel', cancelHandler );
}
return () => {
if ( paymentRequest ) {
shippingAddressChangeEvent.removeAllListeners();
shippingOptionChangeEvent.removeAllListeners();
sourceChangeEvent.removeAllListeners();
cancelChangeEvent.removeAllListeners();
}
};
}, [
paymentRequest,
canMakePayment,
isProcessing,
onClose,
setPaymentRequestEventHandler,
setExpressPaymentError,
onSubmit,
] );
return {
paymentRequest,
paymentRequestEventHandlers,
clearPaymentRequestEventHandler,
isProcessing,
canMakePayment,
onButtonClick,
abortPayment,
completePayment,
paymentRequestType: currentPaymentRequestType.current,
};
};