* Add errors to UI in onCheckoutAfterProcessingWithError

* Fix missing useEffect dependency

* Typo

* Reset source id if Stripe intent fails

* Remove default error on onCheckoutAfterProcessingWithError

* Add missing useEffect dependency

* Only add default error message if there is no errors in any other context

* Fix useEffect running too many times

* Add type-defs and minor improvements

* Only count error notices
This commit is contained in:
Albert Juhé Lluveras 2020-10-20 11:50:33 +02:00 committed by GitHub
parent b28c2f56e8
commit 4f60b6367e
6 changed files with 164 additions and 26 deletions

View File

@ -11,7 +11,12 @@ import {
useCallback, useCallback,
} from '@wordpress/element'; } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useStoreNotices, useEmitResponse } from '@woocommerce/base-hooks'; import {
useCheckoutNotices,
useStoreNotices,
useEmitResponse,
usePrevious,
} from '@woocommerce/base-hooks';
/** /**
* Internal dependencies * Internal dependencies
@ -103,6 +108,11 @@ export const CheckoutStateProvider = ( {
isFailResponse, isFailResponse,
shouldRetry, shouldRetry,
} = useEmitResponse(); } = useEmitResponse();
const {
checkoutNotices,
paymentNotices,
expressPaymentNotices,
} = useCheckoutNotices();
// set observers on ref so it's always current. // set observers on ref so it's always current.
useEffect( () => { useEffect( () => {
@ -210,7 +220,16 @@ export const CheckoutStateProvider = ( {
dispatch, dispatch,
] ); ] );
const previousStatus = usePrevious( checkoutState.status );
const previousHasError = usePrevious( checkoutState.hasError );
useEffect( () => { useEffect( () => {
if (
checkoutState.status === previousStatus &&
checkoutState.hasError === previousHasError
) {
return;
}
if ( checkoutState.status === STATUS.AFTER_PROCESSING ) { if ( checkoutState.status === STATUS.AFTER_PROCESSING ) {
const data = { const data = {
redirectUrl: checkoutState.redirectUrl, redirectUrl: checkoutState.redirectUrl,
@ -244,17 +263,30 @@ export const CheckoutStateProvider = ( {
dispatch( actions.setIdle() ); dispatch( actions.setIdle() );
} }
} else { } else {
// no error handling in place by anything so let's fall const hasErrorNotices =
// back to default checkoutNotices.some(
const message = ( notice ) => notice.status === 'error'
data.processingResponse?.message || ) ||
__( expressPaymentNotices.some(
'Something went wrong. Please contact us to get assistance.', ( notice ) => notice.status === 'error'
'woo-gutenberg-products-block' ) ||
paymentNotices.some(
( notice ) => notice.status === 'error'
); );
addErrorNotice( message, { if ( ! hasErrorNotices ) {
id: 'checkout', // no error handling in place by anything so let's fall
} ); // back to default
const message =
data.processingResponse?.message ||
__(
'Something went wrong. Please contact us to get assistance.',
'woo-gutenberg-products-block'
);
addErrorNotice( message, {
id: 'checkout',
} );
}
dispatch( actions.setIdle() ); dispatch( actions.setIdle() );
} }
} ); } );
@ -280,7 +312,7 @@ export const CheckoutStateProvider = ( {
dispatch( actions.setComplete( response ) ); dispatch( actions.setComplete( response ) );
} else { } else {
// this will set an error which will end up // this will set an error which will end up
// triggering the onCheckoutAfterProcessingWithErrors emitter. // triggering the onCheckoutAfterProcessingWithError emitter.
// and then setting checkout to IDLE state. // and then setting checkout to IDLE state.
dispatch( actions.setHasError( true ) ); dispatch( actions.setHasError( true ) );
} }
@ -300,11 +332,17 @@ export const CheckoutStateProvider = ( {
checkoutState.customerId, checkoutState.customerId,
checkoutState.customerNote, checkoutState.customerNote,
checkoutState.processingResponse, checkoutState.processingResponse,
previousStatus,
previousHasError,
dispatchActions, dispatchActions,
addErrorNotice, addErrorNotice,
isErrorResponse, isErrorResponse,
isFailResponse, isFailResponse,
isSuccessResponse, isSuccessResponse,
shouldRetry,
checkoutNotices,
expressPaymentNotices,
paymentNotices,
] ); ] );
const onSubmit = useCallback( () => { const onSubmit = useCallback( () => {

View File

@ -1,4 +1,5 @@
export * from './use-checkout-redirect-url'; export * from './use-checkout-redirect-url';
export * from './use-checkout-address'; export * from './use-checkout-address';
export * from './use-checkout-notices';
export * from './use-checkout-submit'; export * from './use-checkout-submit';
export * from './use-emit-response'; export * from './use-emit-response';

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { useEmitResponse } from '@woocommerce/base-hooks';
import { useSelect } from '@wordpress/data';
/**
* @typedef {import('@woocommerce/type-defs/contexts').StoreNoticeObject} StoreNoticeObject
* @typedef {import('@woocommerce/type-defs/hooks').CheckoutNotices} CheckoutNotices
*/
/**
* A hook that returns all notices visible in the Checkout block.
*
* @return {CheckoutNotices} Notices from the checkout form or payment methods.
*/
export const useCheckoutNotices = () => {
const { noticeContexts } = useEmitResponse();
/**
* @type {StoreNoticeObject[]}
*/
const checkoutNotices = useSelect(
( select ) => select( 'core/notices' ).getNotices( 'wc/checkout' ),
[]
);
/**
* @type {StoreNoticeObject[]}
*/
const expressPaymentNotices = useSelect(
( select ) =>
select( 'core/notices' ).getNotices(
noticeContexts.EXPRESS_PAYMENTS
),
[ noticeContexts.EXPRESS_PAYMENTS ]
);
/**
* @type {StoreNoticeObject[]}
*/
const paymentNotices = useSelect(
( select ) =>
select( 'core/notices' ).getNotices( noticeContexts.PAYMENTS ),
[ noticeContexts.PAYMENTS ]
);
return {
checkoutNotices,
expressPaymentNotices,
paymentNotices,
};
};

View File

@ -56,6 +56,7 @@ export const useCheckoutSubscriptions = (
usePaymentIntents( usePaymentIntents(
stripe, stripe,
onCheckoutAfterProcessingWithSuccess, onCheckoutAfterProcessingWithSuccess,
setSourceId,
emitResponse emitResponse
); );
usePaymentProcessing( usePaymentProcessing(

View File

@ -11,14 +11,22 @@ import { useEffect } from '@wordpress/element';
/** /**
* Opens the modal for PaymentIntent authorizations. * Opens the modal for PaymentIntent authorizations.
* *
* @param {Stripe} stripe The stripe object. * @param {Object} params Params object.
* @param {Object} paymentDetails The payment details from the server after checkout * @param {Stripe} params.stripe The stripe object.
* processing. * @param {Object} params.paymentDetails The payment details from the
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer response * server after checkout processing.
* objects. * @param {string} params.errorContext Context where errors will be added.
* @param {string} params.errorType Type of error responses.
* @param {string} params.successType Type of success responses.
*/ */
const openIntentModal = ( stripe, paymentDetails, emitResponse ) => { const openIntentModal = ( {
const checkoutResponse = { type: emitResponse.responseTypes.SUCCESS }; stripe,
paymentDetails,
errorContext,
errorType,
successType,
} ) => {
const checkoutResponse = { type: successType };
if ( if (
! paymentDetails.setup_intent && ! paymentDetails.setup_intent &&
! paymentDetails.payment_intent_secret ! paymentDetails.payment_intent_secret
@ -49,23 +57,48 @@ const openIntentModal = ( stripe, paymentDetails, emitResponse ) => {
return checkoutResponse; return checkoutResponse;
} ) } )
.catch( function ( error ) { .catch( function ( error ) {
checkoutResponse.type = emitResponse.responseTypes.ERROR; checkoutResponse.type = errorType;
checkoutResponse.message = error.message; checkoutResponse.message = error.message;
checkoutResponse.retry = true; checkoutResponse.retry = true;
checkoutResponse.messageContext = checkoutResponse.messageContext = errorContext;
emitResponse.noticeContexts.PAYMENTS;
// Reports back to the server. // Reports back to the server.
window.fetch( verificationUrl + '&is_ajax' ); window.fetch( verificationUrl + '&is_ajax' );
return checkoutResponse; return checkoutResponse;
} ); } );
}; };
export const usePaymentIntents = ( stripe, subscriber, emitResponse ) => { export const usePaymentIntents = (
stripe,
subscriber,
setSourceId,
emitResponse
) => {
useEffect( () => { useEffect( () => {
const unsubscribe = subscriber( ( { processingResponse } ) => { const unsubscribe = subscriber( async ( { processingResponse } ) => {
const paymentDetails = processingResponse.paymentDetails || {}; const paymentDetails = processingResponse.paymentDetails || {};
return openIntentModal( stripe, paymentDetails, emitResponse ); const response = await openIntentModal( {
stripe,
paymentDetails,
errorContext: emitResponse.noticeContexts.PAYMENTS,
errorType: emitResponse.responseTypes.ERROR,
successType: emitResponse.responseTypes.SUCCESS,
} );
if (
response.type === emitResponse.responseTypes.ERROR &&
response.retry
) {
setSourceId( '0' );
}
return response;
} ); } );
return () => unsubscribe(); return () => unsubscribe();
}, [ subscriber, stripe ] ); }, [
subscriber,
emitResponse.noticeContexts.PAYMENTS,
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
setSourceId,
stripe,
] );
}; };

View File

@ -1,6 +1,7 @@
/** /**
* @typedef {import('./cart').CartData} CartData * @typedef {import('./cart').CartData} CartData
* @typedef {import('./cart').CartShippingAddress} CartShippingAddress * @typedef {import('./cart').CartShippingAddress} CartShippingAddress
* @typedef {import('./contexts').StoreNoticeObject} StoreNoticeObject
*/ */
/** /**
@ -75,6 +76,17 @@
* the cart. * the cart.
*/ */
/**
* @typedef {Object} CheckoutNotices
*
* @property {StoreNoticeObject[]} checkoutNotices Array of notices in the
* checkout context.
* @property {StoreNoticeObject[]} expressPaymentNotices Array of notices in the
* express payment context.
* @property {StoreNoticeObject[]} paymentNotices Array of notices in the
* payment context.
*/
/** /**
* @typedef {Object} EmitResponseTypes * @typedef {Object} EmitResponseTypes
* *