Fix multiple extra stripe createSource requests than necessary (https://github.com/woocommerce/woocommerce-blocks/pull/2444)

* refactor to move payment processing into it’s own effect

- this is the first step in trying to improve the createSource behaviour.

* memoize setValidationErrors so onPaymentProcessing event effect doesn’t re-run unnecessarily.

* Update assets/js/payment-method-extensions/payment-methods/stripe/credit-card/use-payment-processing.js

Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>

* fix docs alignment

Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
This commit is contained in:
Darren Ethier 2020-05-08 11:32:20 -04:00 committed by GitHub
parent 657e5005ff
commit ea3d817db1
4 changed files with 219 additions and 137 deletions

View File

@ -102,7 +102,7 @@ export const ValidationContextProvider = ( { children } ) => {
* validation error is for and values are the
* validation error message displayed to the user.
*/
const setValidationErrors = ( newErrors ) => {
const setValidationErrors = useCallback( ( newErrors ) => {
if ( ! newErrors ) {
return;
}
@ -124,7 +124,7 @@ export const ValidationContextProvider = ( { children } ) => {
...newErrors,
};
} );
};
}, [] );
/**
* Used to update a validation error.

View File

@ -9,7 +9,7 @@ import { InlineCard, CardElements } from './elements';
/**
* External dependencies
*/
import { Elements, useElements, useStripe } from '@stripe/react-stripe-js';
import { Elements, useStripe } from '@stripe/react-stripe-js';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
@ -32,12 +32,11 @@ const CreditCardComponent = ( {
} ) => {
const { ValidationInputError, CheckboxControl } = components;
const { customerId } = billing;
const [ sourceId, setSourceId ] = useState( 0 );
const [ sourceId, setSourceId ] = useState( '' );
const stripe = useStripe();
const [ shouldSavePayment, setShouldSavePayment ] = useState(
customerId ? true : false
);
const elements = useElements();
const onStripeError = useCheckoutSubscriptions(
eventRegistration,
billing,
@ -45,8 +44,7 @@ const CreditCardComponent = ( {
setSourceId,
shouldSavePayment,
emitResponse,
stripe,
elements
stripe
);
const onChange = ( paymentEvent ) => {
if ( paymentEvent.error ) {

View File

@ -1,39 +1,34 @@
/**
* External dependencies
*/
import { useEffect, useRef, useState } from '@wordpress/element';
import { CardElement, CardNumberElement } from '@stripe/react-stripe-js';
import { useEffect, useCallback, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME } from './constants';
import {
getStripeServerData,
getErrorMessageForTypeAndCode,
} from '../stripe-utils';
import { getErrorMessageForTypeAndCode } from '../stripe-utils';
import { usePaymentIntents } from './use-payment-intents';
import { usePaymentProcessing } from './use-payment-processing';
/**
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
* @typedef {import('react').Dispatch<number>} SourceIdDispatch
* @typedef {import('react').Dispatch<string>} SourceIdDispatch
*/
/**
* A custom hook for the Stripe processing and event observer logic.
*
* @param {EventRegistrationProps} eventRegistration Event registration functions.
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer
* response objects.
* @param {BillingDataProps} billing Various billing data items.
* @param {number} sourceId Current set stripe source id.
* @param {SourceIdDispatch} setSourceId Setter for stripe source id.
* @param {boolean} shouldSavePayment Whether to save the payment or not.
* @param {Stripe} stripe The stripe.js object.
* @param {Object} elements Stripe Elements object.
* @param {EventRegistrationProps} eventRegistration Event registration functions.
* @param {BillingDataProps} billing Various billing data items.
* @param {string} sourceId Current set stripe source id.
* @param {SourceIdDispatch} setSourceId Setter for stripe source id.
* @param {boolean} shouldSavePayment Whether to save the payment or not.
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer
* response objects.
* @param {Stripe} stripe The stripe.js object.
*
* @return {function(Object):Object} Returns a function for handling stripe error.
*/
@ -44,113 +39,40 @@ export const useCheckoutSubscriptions = (
setSourceId,
shouldSavePayment,
emitResponse,
stripe,
elements
stripe
) => {
const [ error, setError ] = useState( '' );
const onStripeError = useRef( ( event ) => {
return event;
} );
const onStripeError = useCallback( ( event ) => {
const type = event.error.type;
const code = event.error.code || '';
const message =
getErrorMessageForTypeAndCode( type, code ) ?? event.error.message;
setError( message );
return message;
}, [] );
const {
onCheckoutAfterProcessingWithSuccess,
onPaymentProcessing,
onCheckoutAfterProcessingWithError,
} = eventRegistration;
usePaymentIntents(
stripe,
eventRegistration.onCheckoutAfterProcessingWithSuccess,
onCheckoutAfterProcessingWithSuccess,
emitResponse
);
usePaymentProcessing(
onStripeError,
error,
stripe,
billing,
emitResponse,
sourceId,
setSourceId,
shouldSavePayment,
onPaymentProcessing
);
// hook into and register callbacks for events.
useEffect( () => {
onStripeError.current = ( event ) => {
const type = event.error.type;
const code = event.error.code || '';
const message =
getErrorMessageForTypeAndCode( type, code ) ??
event.error.message;
setError( error );
return message;
};
const createSource = async ( ownerInfo ) => {
const elementToGet = getStripeServerData().inline_cc_form
? CardElement
: CardNumberElement;
return await stripe.createSource(
elements.getElement( elementToGet ),
{
type: 'card',
owner: ownerInfo,
}
);
};
const onSubmit = async () => {
try {
const { billingData } = billing;
// if there's an error return that.
if ( error ) {
return {
type: emitResponse.responseTypes.ERROR,
message: error,
};
}
// use token if it's set.
if ( sourceId !== 0 ) {
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paymentMethod: PAYMENT_METHOD_NAME,
paymentRequestType: 'cc',
stripe_source: sourceId,
shouldSavePayment,
},
billingData,
},
};
}
const ownerInfo = {
address: {
line1: billingData.address_1,
line2: billingData.address_2,
city: billingData.city,
state: billingData.state,
postal_code: billingData.postcode,
country: billingData.country,
},
};
if ( billingData.phone ) {
ownerInfo.phone = billingData.phone;
}
if ( billingData.email ) {
ownerInfo.email = billingData.email;
}
if ( billingData.first_name || billingData.last_name ) {
ownerInfo.name = `${ billingData.first_name } ${ billingData.last_name }`;
}
const response = await createSource( ownerInfo );
if ( response.error ) {
return {
type: emitResponse.responseTypes.ERROR,
message: onStripeError.current( response ),
};
}
setSourceId( response.source.id );
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
stripe_source: response.source.id,
paymentMethod: PAYMENT_METHOD_NAME,
paymentRequestType: 'cc',
shouldSavePayment,
},
billingData,
},
};
} catch ( e ) {
return {
type: emitResponse.responseTypes.ERROR,
message: e,
};
}
};
const onError = ( { processingResponse } ) => {
if ( processingResponse?.paymentDetails?.errorMessage ) {
return {
@ -162,25 +84,16 @@ export const useCheckoutSubscriptions = (
// so we don't break the observers.
return true;
};
const unsubscribeProcessing = eventRegistration.onPaymentProcessing(
onSubmit
);
const unsubscribeAfterProcessing = eventRegistration.onCheckoutAfterProcessingWithError(
const unsubscribeAfterProcessing = onCheckoutAfterProcessingWithError(
onError
);
return () => {
unsubscribeProcessing();
unsubscribeAfterProcessing();
};
}, [
eventRegistration.onPaymentProcessing,
eventRegistration.onCheckoutAfterProcessingWithError,
stripe,
sourceId,
billing.billingData,
setSourceId,
shouldSavePayment,
error,
onCheckoutAfterProcessingWithError,
emitResponse.noticeContexts.PAYMENTS,
emitResponse.responseTypes.ERROR,
] );
return onStripeError.current;
return onStripeError;
};

View File

@ -0,0 +1,171 @@
/**
* External dependencies
*/
import { useEffect } from '@wordpress/element';
import {
CardElement,
CardNumberElement,
useElements,
} from '@stripe/react-stripe-js';
/**
* Internal dependencies
*/
import { PAYMENT_METHOD_NAME } from './constants';
import {
getStripeServerData,
getErrorMessageForTypeAndCode,
} from '../stripe-utils';
import { errorTypes } from '../stripe-utils/constants';
/**
* @typedef {import('@stripe/stripe-js').Stripe} Stripe
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
* @typedef {import('react').Dispatch<string>} SourceIdDispatch
*/
/**
* @typedef {function(function():any):function():void} EventRegistration
*/
/**
* A custom hook that registers stripe payment processing with the
* onPaymentProcessing event from checkout.
*
* @param {function(any):string} onStripeError Sets an error for stripe.
* @param {string} error Any set error message (an empty string if no
* error).
* @param {Stripe} stripe The stripe utility
* @param {BillingDataProps} billing Various billing data items.
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer
* response objects.
* @param {string} sourceId Current set stripe source id.
* @param {SourceIdDispatch} setSourceId Setter for stripe source id.
* @param {boolean} shouldSavePayment Whether to save the payment or not.
* @param {EventRegistration} onPaymentProcessing The event emitter for processing payment.
*/
export const usePaymentProcessing = (
onStripeError,
error,
stripe,
billing,
emitResponse,
sourceId,
setSourceId,
shouldSavePayment,
onPaymentProcessing
) => {
const elements = useElements();
// hook into and register callbacks for events
useEffect( () => {
const createSource = async ( ownerInfo ) => {
const elementToGet = getStripeServerData().inline_cc_form
? CardElement
: CardNumberElement;
return await stripe.createSource(
// @ts-ignore
elements?.getElement( elementToGet ),
{
type: 'card',
owner: ownerInfo,
}
);
};
const onSubmit = async () => {
try {
const billingData = billing.billingData;
// if there's an error return that.
if ( error ) {
return {
type: emitResponse.responseTypes.ERROR,
message: error,
};
}
// use token if it's set.
if ( parseInt( sourceId, 10 ) !== 0 ) {
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
paymentMethod: PAYMENT_METHOD_NAME,
paymentRequestType: 'cc',
stripe_source: sourceId,
shouldSavePayment,
},
billingData,
},
};
}
const ownerInfo = {
address: {
line1: billingData.address_1,
line2: billingData.address_2,
city: billingData.city,
state: billingData.state,
postal_code: billingData.postcode,
country: billingData.country,
},
};
if ( billingData.phone ) {
ownerInfo.phone = billingData.phone;
}
if ( billingData.email ) {
ownerInfo.email = billingData.email;
}
if ( billingData.first_name || billingData.last_name ) {
ownerInfo.name = `${ billingData.first_name } ${ billingData.last_name }`;
}
const response = await createSource( ownerInfo );
if ( response.error ) {
return {
type: emitResponse.responseTypes.ERROR,
message: onStripeError( response ),
};
}
if ( ! response.source || ! response.source.id ) {
throw new Error(
getErrorMessageForTypeAndCode( errorTypes.API_ERROR )
);
}
setSourceId( response.source.id );
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
stripe_source: response.source.id,
paymentMethod: PAYMENT_METHOD_NAME,
paymentRequestType: 'cc',
shouldSavePayment,
},
billingData,
},
};
} catch ( e ) {
return {
type: emitResponse.responseTypes.ERROR,
message: e,
};
}
};
const unsubscribeProcessing = onPaymentProcessing( onSubmit );
return () => {
unsubscribeProcessing();
};
}, [
onPaymentProcessing,
billing.billingData,
stripe,
sourceId,
setSourceId,
shouldSavePayment,
onStripeError,
error,
emitResponse.noticeContexts.PAYMENTS,
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
elements,
] );
};