Don't throw an error when registering a payment method fails (https://github.com/woocommerce/woocommerce-blocks/pull/3134)

* Show all payment methods when it's an admin and let the error boundary handle errors

* Use StoreNoticesContainer in Payment method error boundary so notices have styling

* Filter out saved payment methods for admin users if they don't accept payments

* Simplify update options logic

* For admins, only show payment methods that errored but canPay was not false

* Simplify how new payment method option is appended

* Wrap canMakePayment in a try catch block to handle payment methods that throw an error

* Add an id to payment method error boundary errors

* Add an error boundary to express payment methods

* Hardcode failing content and savePaymentInfo to false if the payment method failed

* Add some new comments

* Add a notice instead of registering the payment method if it fails and user is admin

* Throw error early if stripe failed to load

* Split express and standard payment method error notices

* Don't add payment methods in the editor and instead add a notice

* Fix error id

* Use noticeContext constant

* Add missing JSdoc param

* Remove unnecessary removeNotice
This commit is contained in:
Albert Juhé Lluveras 2020-09-18 12:27:54 +02:00 committed by GitHub
parent 1871b4e573
commit d641d2e1a4
7 changed files with 126 additions and 93 deletions

View File

@ -15,6 +15,7 @@ import {
useEditorContext, useEditorContext,
usePaymentMethodDataContext, usePaymentMethodDataContext,
} from '@woocommerce/base-context'; } from '@woocommerce/base-context';
import PaymentMethodErrorBoundary from './payment-method-error-boundary';
const ExpressPaymentMethods = () => { const ExpressPaymentMethods = () => {
const { isEditor } = useEditorContext(); const { isEditor } = useEditorContext();
@ -59,9 +60,11 @@ const ExpressPaymentMethods = () => {
<li key="noneRegistered">No registered Payment Methods</li> <li key="noneRegistered">No registered Payment Methods</li>
); );
return ( return (
<ul className="wc-block-components-express-payment__event-buttons"> <PaymentMethodErrorBoundary isEditor={ isEditor }>
{ content } <ul className="wc-block-components-express-payment__event-buttons">
</ul> { content }
</ul>
</PaymentMethodErrorBoundary>
); );
}; };

View File

@ -2,8 +2,14 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useExpressPaymentMethods } from '@woocommerce/base-hooks'; import {
import { StoreNoticesProvider } from '@woocommerce/base-context'; useEmitResponse,
useExpressPaymentMethods,
} from '@woocommerce/base-hooks';
import {
StoreNoticesProvider,
useEditorContext,
} from '@woocommerce/base-context';
import Title from '@woocommerce/base-components/title'; import Title from '@woocommerce/base-components/title';
/** /**
@ -11,14 +17,26 @@ import Title from '@woocommerce/base-components/title';
*/ */
import ExpressPaymentMethods from '../express-payment-methods'; import ExpressPaymentMethods from '../express-payment-methods';
import './style.scss'; import './style.scss';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
const CheckoutExpressPayment = () => { const CheckoutExpressPayment = () => {
const { paymentMethods, isInitialized } = useExpressPaymentMethods(); const { paymentMethods, isInitialized } = useExpressPaymentMethods();
const { isEditor } = useEditorContext();
const { noticeContexts } = useEmitResponse();
if ( if (
! isInitialized || ! isInitialized ||
( isInitialized && Object.keys( paymentMethods ).length === 0 ) ( isInitialized && Object.keys( paymentMethods ).length === 0 )
) { ) {
// Make sure errors are shown in the editor and for admins. For example,
// when a payment method fails to register.
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
return (
<StoreNoticesProvider
context={ noticeContexts.EXPRESS_PAYMENTS }
></StoreNoticesProvider>
);
}
return null; return null;
} }
@ -37,7 +55,9 @@ const CheckoutExpressPayment = () => {
</Title> </Title>
</div> </div>
<div className="wc-block-components-express-payment__content"> <div className="wc-block-components-express-payment__content">
<StoreNoticesProvider context="wc/express-payment-area"> <StoreNoticesProvider
context={ noticeContexts.EXPRESS_PAYMENTS }
>
<p> <p>
{ __( { __(
'In a hurry? Use one of our express checkout options below:', 'In a hurry? Use one of our express checkout options below:',

View File

@ -3,7 +3,7 @@
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Component } from 'react'; import { Component } from 'react';
import { Notice } from 'wordpress-components'; import { StoreNoticesContainer } from '@woocommerce/base-components/store-notices-container';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
@ -36,11 +36,15 @@ class PaymentMethodErrorBoundary extends Component {
); );
} }
} }
return ( const notices = [
<Notice isDismissible={ false } status="error"> {
{ errorText } id: '0',
</Notice> content: errorText,
); isDismissible: false,
status: 'error',
},
];
return <StoreNoticesContainer notices={ notices } />;
} }
return this.props.children; return this.props.children;

View File

@ -8,6 +8,7 @@ import {
usePaymentMethodDataContext, usePaymentMethodDataContext,
} from '@woocommerce/base-context'; } from '@woocommerce/base-context';
import RadioControl from '@woocommerce/base-components/radio-control'; import RadioControl from '@woocommerce/base-components/radio-control';
import { getPaymentMethods } from '@woocommerce/blocks-registry';
/** /**
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod * @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
@ -92,6 +93,7 @@ const SavedPaymentMethodOptions = ( { onSelect } ) => {
setActivePaymentMethod, setActivePaymentMethod,
} = usePaymentMethodDataContext(); } = usePaymentMethodDataContext();
const [ selectedToken, setSelectedToken ] = useState( '' ); const [ selectedToken, setSelectedToken ] = useState( '' );
const standardMethods = getPaymentMethods();
/** /**
* @type {Object} Options * @type {Object} Options
@ -99,56 +101,45 @@ const SavedPaymentMethodOptions = ( { onSelect } ) => {
*/ */
const currentOptions = useRef( [] ); const currentOptions = useRef( [] );
useEffect( () => { useEffect( () => {
let options = []; const types = Object.keys( customerPaymentMethods );
const paymentMethodKeys = Object.keys( customerPaymentMethods ); const options = types
if ( paymentMethodKeys.length > 0 ) { .flatMap( ( type ) => {
paymentMethodKeys.forEach( ( type ) => { const typeMethods = customerPaymentMethods[ type ];
const paymentMethods = customerPaymentMethods[ type ]; return typeMethods.map( ( paymentMethod ) => {
if ( paymentMethods.length > 0 ) { const method =
options = options.concat( standardMethods[ paymentMethod.method.gateway ];
paymentMethods.map( ( paymentMethod ) => { if ( ! method?.supports?.savePaymentInfo ) {
const option = return null;
type === 'cc' || type === 'echeck' }
? getCcOrEcheckPaymentMethodOption( const option =
paymentMethod, type === 'cc' || type === 'echeck'
setActivePaymentMethod, ? getCcOrEcheckPaymentMethodOption(
setPaymentStatus paymentMethod,
) setActivePaymentMethod,
: getDefaultPaymentMethodOptions( setPaymentStatus
paymentMethod, )
setActivePaymentMethod, : getDefaultPaymentMethodOptions(
setPaymentStatus paymentMethod,
); setActivePaymentMethod,
if ( setPaymentStatus
paymentMethod.is_default && );
selectedToken === '' if ( paymentMethod.is_default && selectedToken === '' ) {
) { setSelectedToken( paymentMethod.tokenId + '' );
setSelectedToken( paymentMethod.tokenId + '' ); option.onChange( paymentMethod.tokenId );
option.onChange( paymentMethod.tokenId ); }
} return option;
return option;
} )
);
}
} );
if ( options.length > 0 ) {
currentOptions.current = options;
currentOptions.current.push( {
value: '0',
label: __(
'Use a new payment method',
'woo-gutenberg-product-blocks'
),
name: `wc-saved-payment-method-token-new`,
} ); } );
} } )
} .filter( Boolean );
currentOptions.current = options;
}, [ }, [
customerPaymentMethods, customerPaymentMethods,
selectedToken, selectedToken,
setActivePaymentMethod, setActivePaymentMethod,
setPaymentStatus, setPaymentStatus,
standardMethods,
] ); ] );
const updateToken = useCallback( const updateToken = useCallback(
( token ) => { ( token ) => {
if ( token === '0' ) { if ( token === '0' ) {
@ -167,12 +158,17 @@ const SavedPaymentMethodOptions = ( { onSelect } ) => {
// In the editor, show `Use a new payment method` option as selected. // In the editor, show `Use a new payment method` option as selected.
const selectedOption = isEditor ? '0' : selectedToken + ''; const selectedOption = isEditor ? '0' : selectedToken + '';
const newPaymentMethodOption = {
value: '0',
label: __( 'Use a new payment method', 'woo-gutenberg-product-blocks' ),
name: `wc-saved-payment-method-token-new`,
};
return currentOptions.current.length > 0 ? ( return currentOptions.current.length > 0 ? (
<RadioControl <RadioControl
id={ 'wc-payment-method-saved-tokens' } id={ 'wc-payment-method-saved-tokens' }
selected={ selectedOption } selected={ selectedOption }
onChange={ updateToken } onChange={ updateToken }
options={ currentOptions.current } options={ [ ...currentOptions.current, newPaymentMethodOption ] }
/> />
) : null; ) : null;
}; };

View File

@ -11,32 +11,17 @@ import {
useEditorContext, useEditorContext,
useShippingDataContext, useShippingDataContext,
} from '@woocommerce/base-context'; } from '@woocommerce/base-context';
import { useStoreCart, useShallowEqual } from '@woocommerce/base-hooks'; import {
useEmitResponse,
useShallowEqual,
useStoreCart,
useStoreNotices,
} from '@woocommerce/base-hooks';
import { import {
CURRENT_USER_IS_ADMIN, CURRENT_USER_IS_ADMIN,
PAYMENT_GATEWAY_SORT_ORDER, PAYMENT_GATEWAY_SORT_ORDER,
} from '@woocommerce/block-settings'; } from '@woocommerce/block-settings';
/**
* If there was an error registering a payment method, alert the admin.
*
* @param {Object} error Error object.
*/
const handleRegistrationError = ( error ) => {
if ( CURRENT_USER_IS_ADMIN ) {
throw new Error(
sprintf(
__(
// translators: %s is the error method returned by the payment method.
'Problem with payment method initialization: %s',
'woo-gutenberg-products-block'
),
error.message
)
);
}
};
/** /**
* This hook handles initializing registered payment methods and exposing all * This hook handles initializing registered payment methods and exposing all
* registered payment methods that can be used in the current environment (via * registered payment methods that can be used in the current environment (via
@ -50,6 +35,8 @@ const handleRegistrationError = ( error ) => {
* @param {Array} paymentMethodsSortOrder Array of payment method names to * @param {Array} paymentMethodsSortOrder Array of payment method names to
* sort by. This should match keys of * sort by. This should match keys of
* registeredPaymentMethods. * registeredPaymentMethods.
* @param {string} noticeContext Id of the context to append
* notices to.
* *
* @return {boolean} Whether the payment methods have been initialized or not. True when all payment * @return {boolean} Whether the payment methods have been initialized or not. True when all payment
* methods have been initialized. * methods have been initialized.
@ -57,7 +44,8 @@ const handleRegistrationError = ( error ) => {
const usePaymentMethodRegistration = ( const usePaymentMethodRegistration = (
dispatcher, dispatcher,
registeredPaymentMethods, registeredPaymentMethods,
paymentMethodsSortOrder paymentMethodsSortOrder,
noticeContext
) => { ) => {
const [ isInitialized, setIsInitialized ] = useState( false ); const [ isInitialized, setIsInitialized ] = useState( false );
const { isEditor } = useEditorContext(); const { isEditor } = useEditorContext();
@ -71,6 +59,7 @@ const usePaymentMethodRegistration = (
shippingAddress, shippingAddress,
selectedShippingMethods, selectedShippingMethods,
} ); } );
const { addErrorNotice } = useStoreNotices();
useEffect( () => { useEffect( () => {
canPayArgument.current = { canPayArgument.current = {
@ -102,12 +91,6 @@ const usePaymentMethodRegistration = (
continue; continue;
} }
// In editor, shortcut so all payment methods show as available.
if ( isEditor ) {
addAvailablePaymentMethod( paymentMethod );
continue;
}
// In front end, ask payment method if it should be available. // In front end, ask payment method if it should be available.
try { try {
const canPay = await Promise.resolve( const canPay = await Promise.resolve(
@ -120,8 +103,20 @@ const usePaymentMethodRegistration = (
addAvailablePaymentMethod( paymentMethod ); addAvailablePaymentMethod( paymentMethod );
} }
} catch ( e ) { } catch ( e ) {
// If user is admin, show payment `canMakePayment` errors as a notice. if ( CURRENT_USER_IS_ADMIN || isEditor ) {
handleRegistrationError( e ); const errorText = sprintf(
/* translators: %s the id of the payment method being registered (bank transfer, Stripe...) */
__(
`There was an error registering the payment method with id '%s': `,
'woo-gutenberg-products-block'
),
paymentMethod.paymentMethodId
);
addErrorNotice( `${ errorText } ${ e }`, {
context: noticeContext,
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
} );
}
} }
} }
@ -133,10 +128,12 @@ const usePaymentMethodRegistration = (
// That's why we track "is initialised" state here. // That's why we track "is initialised" state here.
setIsInitialized( true ); setIsInitialized( true );
}, [ }, [
addErrorNotice,
dispatcher, dispatcher,
isEditor, isEditor,
registeredPaymentMethods, noticeContext,
paymentMethodsOrder, paymentMethodsOrder,
registeredPaymentMethods,
] ); ] );
// Determine which payment methods are available initially and whenever // Determine which payment methods are available initially and whenever
@ -158,6 +155,7 @@ const usePaymentMethodRegistration = (
*/ */
export const usePaymentMethods = ( dispatcher ) => { export const usePaymentMethods = ( dispatcher ) => {
const standardMethods = getPaymentMethods(); const standardMethods = getPaymentMethods();
const { noticeContexts } = useEmitResponse();
// Ensure all methods are present in order. // Ensure all methods are present in order.
// Some payment methods may not be present in PAYMENT_GATEWAY_SORT_ORDER if they // Some payment methods may not be present in PAYMENT_GATEWAY_SORT_ORDER if they
// depend on state, e.g. COD can depend on shipping method. // depend on state, e.g. COD can depend on shipping method.
@ -168,7 +166,8 @@ export const usePaymentMethods = ( dispatcher ) => {
return usePaymentMethodRegistration( return usePaymentMethodRegistration(
dispatcher, dispatcher,
standardMethods, standardMethods,
Array.from( displayOrder ) Array.from( displayOrder ),
noticeContexts.PAYMENTS
); );
}; };
@ -181,9 +180,11 @@ export const usePaymentMethods = ( dispatcher ) => {
*/ */
export const useExpressPaymentMethods = ( dispatcher ) => { export const useExpressPaymentMethods = ( dispatcher ) => {
const expressMethods = getExpressPaymentMethods(); const expressMethods = getExpressPaymentMethods();
const { noticeContexts } = useEmitResponse();
return usePaymentMethodRegistration( return usePaymentMethodRegistration(
dispatcher, dispatcher,
expressMethods, expressMethods,
Object.keys( expressMethods ) Object.keys( expressMethods ),
noticeContexts.EXPRESS_PAYMENTS
); );
}; };

View File

@ -21,7 +21,11 @@ import {
import { getAdminLink } from '@woocommerce/settings'; import { getAdminLink } from '@woocommerce/settings';
import { __experimentalCreateInterpolateElement } from 'wordpress-element'; import { __experimentalCreateInterpolateElement } from 'wordpress-element';
import { useRef } from '@wordpress/element'; import { useRef } from '@wordpress/element';
import { EditorProvider, useEditorContext } from '@woocommerce/base-context'; import {
EditorProvider,
useEditorContext,
StoreNoticesProvider,
} from '@woocommerce/base-context';
import PageSelector from '@woocommerce/editor-components/page-selector'; import PageSelector from '@woocommerce/editor-components/page-selector';
import { import {
previewCart, previewCart,
@ -320,9 +324,11 @@ const CheckoutEditor = ( { attributes, setAttributes } ) => {
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'
) } ) }
> >
<Disabled> <StoreNoticesProvider context="wc/checkout">
<Block attributes={ attributes } /> <Disabled>
</Disabled> <Block attributes={ attributes } />
</Disabled>
</StoreNoticesProvider>
</BlockErrorBoundary> </BlockErrorBoundary>
</div> </div>
</EditorProvider> </EditorProvider>

View File

@ -32,6 +32,9 @@ function paymentRequestAvailable( currencyCode ) {
isStripeInitialized = true; isStripeInitialized = true;
return canPay; return canPay;
} }
if ( stripe.error && stripe.error instanceof Error ) {
throw stripe.error;
}
// Do a test payment to confirm if payment method is available. // Do a test payment to confirm if payment method is available.
const paymentRequest = stripe.paymentRequest( { const paymentRequest = stripe.paymentRequest( {
total: { total: {