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

View File

@ -2,8 +2,14 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useExpressPaymentMethods } from '@woocommerce/base-hooks';
import { StoreNoticesProvider } from '@woocommerce/base-context';
import {
useEmitResponse,
useExpressPaymentMethods,
} from '@woocommerce/base-hooks';
import {
StoreNoticesProvider,
useEditorContext,
} from '@woocommerce/base-context';
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 './style.scss';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
const CheckoutExpressPayment = () => {
const { paymentMethods, isInitialized } = useExpressPaymentMethods();
const { isEditor } = useEditorContext();
const { noticeContexts } = useEmitResponse();
if (
! isInitialized ||
( 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;
}
@ -37,7 +55,9 @@ const CheckoutExpressPayment = () => {
</Title>
</div>
<div className="wc-block-components-express-payment__content">
<StoreNoticesProvider context="wc/express-payment-area">
<StoreNoticesProvider
context={ noticeContexts.EXPRESS_PAYMENTS }
>
<p>
{ __(
'In a hurry? Use one of our express checkout options below:',

View File

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

View File

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

View File

@ -11,32 +11,17 @@ import {
useEditorContext,
useShippingDataContext,
} from '@woocommerce/base-context';
import { useStoreCart, useShallowEqual } from '@woocommerce/base-hooks';
import {
useEmitResponse,
useShallowEqual,
useStoreCart,
useStoreNotices,
} from '@woocommerce/base-hooks';
import {
CURRENT_USER_IS_ADMIN,
PAYMENT_GATEWAY_SORT_ORDER,
} 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
* 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
* sort by. This should match keys of
* 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
* methods have been initialized.
@ -57,7 +44,8 @@ const handleRegistrationError = ( error ) => {
const usePaymentMethodRegistration = (
dispatcher,
registeredPaymentMethods,
paymentMethodsSortOrder
paymentMethodsSortOrder,
noticeContext
) => {
const [ isInitialized, setIsInitialized ] = useState( false );
const { isEditor } = useEditorContext();
@ -71,6 +59,7 @@ const usePaymentMethodRegistration = (
shippingAddress,
selectedShippingMethods,
} );
const { addErrorNotice } = useStoreNotices();
useEffect( () => {
canPayArgument.current = {
@ -102,12 +91,6 @@ const usePaymentMethodRegistration = (
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.
try {
const canPay = await Promise.resolve(
@ -120,8 +103,20 @@ const usePaymentMethodRegistration = (
addAvailablePaymentMethod( paymentMethod );
}
} catch ( e ) {
// If user is admin, show payment `canMakePayment` errors as a notice.
handleRegistrationError( e );
if ( CURRENT_USER_IS_ADMIN || isEditor ) {
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.
setIsInitialized( true );
}, [
addErrorNotice,
dispatcher,
isEditor,
registeredPaymentMethods,
noticeContext,
paymentMethodsOrder,
registeredPaymentMethods,
] );
// Determine which payment methods are available initially and whenever
@ -158,6 +155,7 @@ const usePaymentMethodRegistration = (
*/
export const usePaymentMethods = ( dispatcher ) => {
const standardMethods = getPaymentMethods();
const { noticeContexts } = useEmitResponse();
// Ensure all methods are present in order.
// 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.
@ -168,7 +166,8 @@ export const usePaymentMethods = ( dispatcher ) => {
return usePaymentMethodRegistration(
dispatcher,
standardMethods,
Array.from( displayOrder )
Array.from( displayOrder ),
noticeContexts.PAYMENTS
);
};
@ -181,9 +180,11 @@ export const usePaymentMethods = ( dispatcher ) => {
*/
export const useExpressPaymentMethods = ( dispatcher ) => {
const expressMethods = getExpressPaymentMethods();
const { noticeContexts } = useEmitResponse();
return usePaymentMethodRegistration(
dispatcher,
expressMethods,
Object.keys( expressMethods )
Object.keys( expressMethods ),
noticeContexts.EXPRESS_PAYMENTS
);
};

View File

@ -21,7 +21,11 @@ import {
import { getAdminLink } from '@woocommerce/settings';
import { __experimentalCreateInterpolateElement } 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 {
previewCart,
@ -320,9 +324,11 @@ const CheckoutEditor = ( { attributes, setAttributes } ) => {
'woo-gutenberg-products-block'
) }
>
<Disabled>
<Block attributes={ attributes } />
</Disabled>
<StoreNoticesProvider context="wc/checkout">
<Disabled>
<Block attributes={ attributes } />
</Disabled>
</StoreNoticesProvider>
</BlockErrorBoundary>
</div>
</EditorProvider>

View File

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