Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update: add error boundary around BNPL messaging element #10164

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/update-add-error-boundary-around-bnpl-messaging
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: update

update: add error boundary around BNPL messaging element
6 changes: 3 additions & 3 deletions client/capital/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { TableCard } from '@woocommerce/components';
*/
import Page from 'components/page';
import { TestModeNotice } from 'components/test-mode-notice';
import ErrorBoundary from 'components/error-boundary';
import AdminErrorBoundary from 'wcpay/components/admin-error-boundary';
import ActiveLoanSummary from 'components/active-loan-summary';
import {
formatExplicitCurrency,
Expand Down Expand Up @@ -212,9 +212,9 @@ const CapitalPage = (): JSX.Element => {
<TestModeNotice currentPage="loans" />

{ wcpaySettings.accountLoans.has_active_loan && (
<ErrorBoundary>
<AdminErrorBoundary>
<ActiveLoanSummary />
</ErrorBoundary>
</AdminErrorBoundary>
) }
<TableCard
className="wcpay-loans-list"
Expand Down
20 changes: 12 additions & 8 deletions client/cart/blocks/product-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { select } from '@wordpress/data';
* Internal dependencies
*/
import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles';
import ErrorBoundary from 'wcpay/components/error-boundary';
import { useStripeAsync } from 'wcpay/hooks/use-stripe-async';
import { getUPEConfig } from 'utils/checkout';
import WCPayAPI from '../../checkout/api';
Expand Down Expand Up @@ -99,21 +100,24 @@ const ProductDetail = ( { cart, context } ) => {
};

return (
<div className="wc-block-components-bnpl-wrapper">
<Elements
stripe={ stripe }
options={ { appearance, fonts: fontRules } }
>
<PaymentMethodMessagingElement options={ options } />
</Elements>
</div>
<ErrorBoundary>
<div className="wc-block-components-bnpl-wrapper">
<Elements
stripe={ stripe }
options={ { appearance, fonts: fontRules } }
>
<PaymentMethodMessagingElement options={ options } />
</Elements>
</div>
</ErrorBoundary>
);
};

export const renderBNPLCartMessaging = () => {
if ( isInEditor() ) {
return null;
}

return (
<ExperimentalOrderMeta>
<ProductDetail />
Expand Down
48 changes: 26 additions & 22 deletions client/checkout/blocks/payment-method-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { __ } from '@wordpress/i18n';
import './style.scss';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles';
import ErrorBoundary from 'wcpay/components/error-boundary';

const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ];
const PaymentMethodMessageWrapper = ( {
Expand Down Expand Up @@ -114,31 +115,34 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => {
alt={ title }
/>
</div>
<PaymentMethodMessageWrapper
upeName={ upeName }
countries={ countries }
amount={ amount }
currentCountry={ currentCountry }
appearance={ appearance }
>
<Elements
stripe={ stripe }
options={ {
appearance: appearance,
fonts: fontRules,
} }
<ErrorBoundary>
<PaymentMethodMessageWrapper
upeName={ upeName }
countries={ countries }
amount={ amount }
currentCountry={ currentCountry }
appearance={ appearance }
>
<PaymentMethodMessagingElement
<Elements
stripe={ stripe }
options={ {
amount: amount || 0,
currency: cartData.totals.currency_code || 'USD',
paymentMethodTypes: [ upeName ],
countryCode: currentCountry,
displayType: 'promotional_text',
appearance: appearance,
fonts: fontRules,
} }
/>
</Elements>
</PaymentMethodMessageWrapper>
>
<PaymentMethodMessagingElement
options={ {
amount: amount || 0,
currency:
cartData.totals.currency_code || 'USD',
paymentMethodTypes: [ upeName ],
countryCode: currentCountry,
displayType: 'promotional_text',
} }
/>
</Elements>
</PaymentMethodMessageWrapper>
</ErrorBoundary>
</>
);
};
34 changes: 34 additions & 0 deletions client/components/admin-error-boundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import InlineNotice from '../inline-notice';
import ErrorBoundary from '../error-boundary';
import React from 'react';

const AdminErrorFallback = ( { error }: { error: any } ) => {
return (
<InlineNotice icon status="error" isDismissible={ false }>
{ __(
'There was an error rendering this view. Please contact support for assistance if the problem persists.',
'woocommerce-payments'
) }
<br />
{ error.toString() }
</InlineNotice>
);
};

const AdminErrorBoundary: React.FC = ( { children } ) => {
return (
<ErrorBoundary fallbackRender={ AdminErrorFallback }>
{ children }
</ErrorBoundary>
);
};

export default AdminErrorBoundary;
90 changes: 90 additions & 0 deletions client/components/error-boundary/__tests__/error-boundary.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* External dependencies
*/
import React, { useEffect } from 'react';
import { render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import ErrorBoundary from '..';

const ComponentThatThrows = () => {
useEffect( () => {
throw new Error( 'Some error' );
} );

return null;
};

describe( 'ErrorBoundary', () => {
const handleError = ( event ) => {
event.preventDefault();
};

beforeAll( () => {
// preventing the error from bubble up to `@wordpress/jest-console`
window.addEventListener( 'error', handleError );
} );

afterAll( () => {
window.removeEventListener( 'error', handleError );
} );

it( 'renders its children', () => {
const onErrorMock = jest.fn();
const fallbackMock = jest.fn().mockReturnValue( 'Fallback message' );

render(
<ErrorBoundary
onError={ onErrorMock }
fallbackRender={ fallbackMock }
>
<p>Children mock</p>
</ErrorBoundary>
);

expect( screen.queryByText( 'Fallback message' ) ).toBeNull();
expect( screen.getByText( 'Children mock' ) ).toBeInTheDocument();
expect( onErrorMock ).not.toHaveBeenCalled();
expect( fallbackMock ).not.toHaveBeenCalled();
} );

it( 'renders nothing on error, if no fallback is provided', () => {
const onErrorMock = jest.fn();

const {
container: { firstChild },
} = render(
<ErrorBoundary onError={ onErrorMock }>
<ComponentThatThrows />
<p>Children mock</p>
</ErrorBoundary>
);

expect( screen.queryByText( 'Children mock' ) ).toBeNull();
expect( firstChild ).toBeNull();
expect( onErrorMock ).toHaveBeenCalledWith(
new Error( 'Some error' ),
expect.objectContaining( { componentStack: expect.anything() } )
);
} );

it( 'renders the fallback on error', () => {
const fallbackMock = jest.fn().mockReturnValue( 'Fallback message' );

render(
<ErrorBoundary fallbackRender={ fallbackMock }>
<ComponentThatThrows />
<p>Children mock</p>
</ErrorBoundary>
);

expect( screen.queryByText( 'Children mock' ) ).toBeNull();
expect( screen.getByText( 'Fallback message' ) ).toBeInTheDocument();
expect( fallbackMock ).toHaveBeenCalledWith(
expect.objectContaining( { error: new Error( 'Some error' ) } ),
expect.anything()
);
} );
} );
54 changes: 38 additions & 16 deletions client/components/error-boundary/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
/**
* WordPress dependencies
* External dependencies
*/
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import InlineNotice from 'components/inline-notice';
import { __, sprintf } from '@wordpress/i18n';

const DevFallback = ( { error } ) => {
if ( process.env.MODE === 'production' ) {
return null;
}

return (
<div
style={ {
padding: '5px 10px',
background: 'papayawhip',
} }
>
{ sprintf(
/* translators: %s: Error message - used in development mode */
__(
'Development error caught by error boundary: %s',
'woocommerce-payments'
),
error.toString()
) }
</div>
);
};

class ErrorBoundary extends Component {
constructor() {
super( ...arguments );
constructor( props ) {
super( props );

this.state = {
error: null,
Expand All @@ -19,26 +42,25 @@ class ErrorBoundary extends Component {
}

componentDidCatch( error, info ) {
// this branch of code will not be present in a production build
if ( process.env.MODE !== 'production' ) {
// eslint-disable-next-line no-console
console.error( error, info );
}

if ( this.props.onError ) {
this.props.onError( error, info );
}
}

render() {
const { children, fallbackRender: Fallback = DevFallback } = this.props;

if ( ! this.state.error ) {
return this.props.children;
return children;
}

return (
<InlineNotice icon status="error" isDismissible={ false }>
{ __(
'There was an error rendering this view. Please contact support for assistance if the problem persists.',
'woocommerce-payments'
) }
<br />
{ this.state.error.toString() }
</InlineNotice>
);
return <Fallback error={ this.state.error } />;
}
}

Expand Down
4 changes: 2 additions & 2 deletions client/components/page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as React from 'react';
* Internal dependencies
*/
import enqueueFraudScripts from 'fraud-scripts';
import ErrorBoundary from '../error-boundary';
import AdminErrorBoundary from '../admin-error-boundary';
import './style.scss';

interface PageProps {
Expand Down Expand Up @@ -41,7 +41,7 @@ const Page: React.FC< PageProps > = ( {

return (
<div className={ classNames.join( ' ' ) } style={ customStyle }>
<ErrorBoundary>{ children }</ErrorBoundary>
<AdminErrorBoundary>{ children }</AdminErrorBoundary>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import interpolateComponents from '@automattic/interpolate-components';
import { Link } from '@woocommerce/components';
import { recordEvent } from 'wcpay/tracks';
import { ClickTooltip } from 'wcpay/components/tooltip';
import ErrorBoundary from 'wcpay/components/error-boundary';
import AdminErrorBoundary from 'wcpay/components/admin-error-boundary';
import SetupLivePaymentsModal from './modal';
import './style.scss';

Expand Down Expand Up @@ -111,13 +111,13 @@ const SandboxModeSwitchToLiveNotice: React.FC< Props > = ( {
} ) }
</BannerNotice>
{ livePaymentsModalVisible && (
<ErrorBoundary>
<AdminErrorBoundary>
<SetupLivePaymentsModal
from={ from }
source={ source }
onClose={ () => setLivePaymentsModalVisible( false ) }
/>
</ErrorBoundary>
</AdminErrorBoundary>
) }
</>
);
Expand Down
Loading
Loading