From 7aced2661a221ddad15305947de51ac9b080ebb0 Mon Sep 17 00:00:00 2001 From: frosso Date: Thu, 16 Jan 2025 13:14:52 +0100 Subject: [PATCH] update: add error boundary around BNPL messaging element --- ...e-add-error-boundary-around-bnpl-messaging | 4 + client/capital/index.tsx | 6 +- client/cart/blocks/product-details.js | 20 +++-- .../checkout/blocks/payment-method-label.js | 48 +++++----- .../components/admin-error-boundary/index.tsx | 34 +++++++ .../__tests__/error-boundary.test.js | 90 +++++++++++++++++++ client/components/error-boundary/index.js | 54 +++++++---- client/components/page/index.tsx | 4 +- .../index.tsx | 6 +- client/deposits/details/index.tsx | 10 +-- client/disputes/evidence/index.js | 14 +-- client/overview/index.js | 38 ++++---- .../payment-details/payment-details/index.tsx | 14 +-- client/payment-details/summary/index.tsx | 6 +- .../buy-now-pay-later-section/index.js | 6 +- .../express-checkout-settings/index.js | 6 +- .../advanced-settings/index.tsx | 6 +- client/settings/index.js | 10 +-- .../settings/payment-methods-section/index.js | 6 +- client/settings/settings-manager/index.js | 26 +++--- webpack/shared.js | 7 +- 21 files changed, 289 insertions(+), 126 deletions(-) create mode 100644 changelog/update-add-error-boundary-around-bnpl-messaging create mode 100644 client/components/admin-error-boundary/index.tsx create mode 100644 client/components/error-boundary/__tests__/error-boundary.test.js diff --git a/changelog/update-add-error-boundary-around-bnpl-messaging b/changelog/update-add-error-boundary-around-bnpl-messaging new file mode 100644 index 00000000000..b6624a033f4 --- /dev/null +++ b/changelog/update-add-error-boundary-around-bnpl-messaging @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +update: add error boundary around BNPL messaging element diff --git a/client/capital/index.tsx b/client/capital/index.tsx index 2a4afa04ff7..2d0e6db0fef 100644 --- a/client/capital/index.tsx +++ b/client/capital/index.tsx @@ -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, @@ -212,9 +212,9 @@ const CapitalPage = (): JSX.Element => { { wcpaySettings.accountLoans.has_active_loan && ( - + - + ) } { }; return ( -
- - - -
+ +
+ + + +
+
); }; @@ -114,6 +117,7 @@ export const renderBNPLCartMessaging = () => { if ( isInEditor() ) { return null; } + return ( diff --git a/client/checkout/blocks/payment-method-label.js b/client/checkout/blocks/payment-method-label.js index acbf9f1ccc0..ea44a5299e1 100644 --- a/client/checkout/blocks/payment-method-label.js +++ b/client/checkout/blocks/payment-method-label.js @@ -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 = ( { @@ -114,31 +115,34 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => { alt={ title } /> - - + - - - + > + + + + ); }; diff --git a/client/components/admin-error-boundary/index.tsx b/client/components/admin-error-boundary/index.tsx new file mode 100644 index 00000000000..795f0e7c9be --- /dev/null +++ b/client/components/admin-error-boundary/index.tsx @@ -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 ( + + { __( + 'There was an error rendering this view. Please contact support for assistance if the problem persists.', + 'woocommerce-payments' + ) } +
+ { error.toString() } +
+ ); +}; + +const AdminErrorBoundary: React.FC = ( { children } ) => { + return ( + + { children } + + ); +}; + +export default AdminErrorBoundary; diff --git a/client/components/error-boundary/__tests__/error-boundary.test.js b/client/components/error-boundary/__tests__/error-boundary.test.js new file mode 100644 index 00000000000..698aecc32cb --- /dev/null +++ b/client/components/error-boundary/__tests__/error-boundary.test.js @@ -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( + +

Children mock

+
+ ); + + 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( + + +

Children mock

+
+ ); + + 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( + + +

Children mock

+
+ ); + + expect( screen.queryByText( 'Children mock' ) ).toBeNull(); + expect( screen.getByText( 'Fallback message' ) ).toBeInTheDocument(); + expect( fallbackMock ).toHaveBeenCalledWith( + expect.objectContaining( { error: new Error( 'Some error' ) } ), + expect.anything() + ); + } ); +} ); diff --git a/client/components/error-boundary/index.js b/client/components/error-boundary/index.js index 6fba86a54d9..1f1a535f23a 100644 --- a/client/components/error-boundary/index.js +++ b/client/components/error-boundary/index.js @@ -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 ( +
+ { sprintf( + /* translators: %s: Error message - used in development mode */ + __( + 'Development error caught by error boundary: %s', + 'woocommerce-payments' + ), + error.toString() + ) } +
+ ); +}; class ErrorBoundary extends Component { - constructor() { - super( ...arguments ); + constructor( props ) { + super( props ); this.state = { error: null, @@ -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 ( - - { __( - 'There was an error rendering this view. Please contact support for assistance if the problem persists.', - 'woocommerce-payments' - ) } -
- { this.state.error.toString() } -
- ); + return ; } } diff --git a/client/components/page/index.tsx b/client/components/page/index.tsx index 82f5c81b757..9c17ca8d530 100644 --- a/client/components/page/index.tsx +++ b/client/components/page/index.tsx @@ -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 { @@ -41,7 +41,7 @@ const Page: React.FC< PageProps > = ( { return (
- { children } + { children }
); }; diff --git a/client/components/sandbox-mode-switch-to-live-notice/index.tsx b/client/components/sandbox-mode-switch-to-live-notice/index.tsx index 23f5552cfa5..6b72b2c9404 100644 --- a/client/components/sandbox-mode-switch-to-live-notice/index.tsx +++ b/client/components/sandbox-mode-switch-to-live-notice/index.tsx @@ -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'; @@ -111,13 +111,13 @@ const SandboxModeSwitchToLiveNotice: React.FC< Props > = ( { } ) } { livePaymentsModalVisible && ( - + setLivePaymentsModalVisible( false ) } /> - + ) } ); diff --git a/client/deposits/details/index.tsx b/client/deposits/details/index.tsx index 6f8ed023da1..4a078303b62 100644 --- a/client/deposits/details/index.tsx +++ b/client/deposits/details/index.tsx @@ -29,7 +29,7 @@ import type { CachedDeposit } from 'types/deposits'; import { useDeposit } from 'data'; import TransactionsList from 'transactions/list'; import Page from 'components/page'; -import ErrorBoundary from 'components/error-boundary'; +import AdminErrorBoundary from 'components/admin-error-boundary'; import { TestModeNotice } from 'components/test-mode-notice'; import InlineNotice from 'components/inline-notice'; import { @@ -242,16 +242,16 @@ export const DepositDetails: React.FC< DepositDetailsProps > = ( { return ( - + { isLoading ? ( ) : ( ) } - + { deposit && ( - + { isInstantDeposit ? ( // If instant deposit, show a message instead of the transactions list. // Matching the components used in @woocommerce/components TableCard for consistent UI. @@ -282,7 +282,7 @@ export const DepositDetails: React.FC< DepositDetailsProps > = ( { ) : ( ) } - + ) } ); diff --git a/client/disputes/evidence/index.js b/client/disputes/evidence/index.js index 940c78c865e..26e85191dd0 100644 --- a/client/disputes/evidence/index.js +++ b/client/disputes/evidence/index.js @@ -30,7 +30,7 @@ import { FileUploadControl, UploadedReadOnly } from 'components/file-upload'; import { TestModeNotice } from 'components/test-mode-notice'; import Info from '../info'; import Page from 'components/page'; -import ErrorBoundary from 'components/error-boundary'; +import AdminErrorBoundary from 'components/admin-error-boundary'; import Loadable, { LoadableBlock } from 'components/loadable'; import useConfirmNavigation from 'utils/use-confirm-navigation'; import { recordEvent } from 'tracks'; @@ -324,7 +324,7 @@ export const DisputeEvidencePage = ( props ) => { { readOnly && ! isLoading && readOnlyNotice } - + { @@ -341,8 +341,8 @@ export const DisputeEvidencePage = ( props ) => { - - + + { @@ -406,17 +406,17 @@ export const DisputeEvidencePage = ( props ) => { - + { // Don't render the form placeholder while the dispute is being loaded. // The form content depends on the selected product type, hence placeholder might disappear after loading. ! isLoading && ( - + - + ) } diff --git a/client/overview/index.js b/client/overview/index.js index f6d5afd4a72..5021f1dffbe 100644 --- a/client/overview/index.js +++ b/client/overview/index.js @@ -17,7 +17,7 @@ import AccountStatus from 'components/account-status'; import ActiveLoanSummary from 'components/active-loan-summary'; import ConnectionSuccessNotice from './connection-sucess-notice'; import DepositsOverview from 'components/deposits-overview'; -import ErrorBoundary from 'components/error-boundary'; +import AdminErrorBoundary from 'components/admin-error-boundary'; import FRTDiscoverabilityBanner from 'components/fraud-risk-tools-banner'; import JetpackIdcNotice from 'components/jetpack-idc-notice'; import Page from 'components/page'; @@ -187,66 +187,66 @@ const OverviewPage = () => { actions={ [] } /> ) } - + - + { showConnectionSuccess && } { ! accountRejected && ! accountUnderReview && ( - + { showTaskList && ( - + - + ) } - + - + { /* Show Payment Activity widget only when feature flag is set. To be removed before go live */ isPaymentOverviewWidgetEnabled && ( - + - + ) } - + ) } - + - + { hasActiveLoan && ( - + - + ) } { ! accountRejected && ! accountUnderReview && ( - + - + ) } { showProgressiveOnboardingEligibilityModal && ( - + - + ) } ); diff --git a/client/payment-details/payment-details/index.tsx b/client/payment-details/payment-details/index.tsx index cdf568520f2..574d7e8c7cd 100644 --- a/client/payment-details/payment-details/index.tsx +++ b/client/payment-details/payment-details/index.tsx @@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n'; import { TestModeNotice } from '../../components/test-mode-notice'; import Page from '../../components/page'; import { Card, CardBody } from '@wordpress/components'; -import ErrorBoundary from '../../components/error-boundary'; +import AdminErrorBoundary from '../../components/admin-error-boundary'; import PaymentDetailsSummary from '../summary'; import PaymentDetailsTimeline from '../timeline'; import PaymentDetailsPaymentMethod from '../payment-method'; @@ -57,27 +57,27 @@ const PaymentDetails: React.FC< PaymentDetailsProps > = ( { return ( - + - + { showTimeline && wcpaySettings.featureFlags.paymentTimeline && ( - + - + ) } - + - + ); }; diff --git a/client/payment-details/summary/index.tsx b/client/payment-details/summary/index.tsx index 59ae556c785..f7fabf6f34c 100644 --- a/client/payment-details/summary/index.tsx +++ b/client/payment-details/summary/index.tsx @@ -60,7 +60,7 @@ import { PaymentIntent } from '../../types/payment-intents'; import MissingOrderNotice from 'wcpay/payment-details/summary/missing-order-notice'; import DisputeAwaitingResponseDetails from '../dispute-details/dispute-awaiting-response-details'; import DisputeResolutionFooter from '../dispute-details/dispute-resolution-footer'; -import ErrorBoundary from 'components/error-boundary'; +import AdminErrorBoundary from 'components/admin-error-boundary'; import RefundModal from 'wcpay/payment-details/summary/refund-modal'; import CardNotice from 'wcpay/components/card-notice'; import { @@ -633,7 +633,7 @@ const PaymentDetailsSummary: React.FC< PaymentDetailsSummaryProps > = ( { { charge.dispute && ( - + { isAwaitingResponse( charge.dispute.status ) ? ( = ( { ) : ( ) } - + ) } { isRefundModalOpen && ( { id="buy-now-pay-later-methods" > - + { /> - + ); diff --git a/client/settings/express-checkout-settings/index.js b/client/settings/express-checkout-settings/index.js index b55628c522a..3690b967403 100644 --- a/client/settings/express-checkout-settings/index.js +++ b/client/settings/express-checkout-settings/index.js @@ -16,7 +16,7 @@ import WooPaySettings from './woopay-settings'; import SettingsLayout from '../settings-layout'; import LoadableSettingsSection from '../loadable-settings-section'; import SaveSettingsSection from '../save-settings-section'; -import ErrorBoundary from '../../components/error-boundary'; +import AdminErrorBoundary from '../../components/admin-error-boundary'; import { ApplePayIcon, GooglePayIcon, @@ -149,9 +149,9 @@ const ExpressCheckoutSettings = ( { methodId } ) => { { sections.map( ( { section, description } ) => ( - + - + ) ) } diff --git a/client/settings/fraud-protection/advanced-settings/index.tsx b/client/settings/fraud-protection/advanced-settings/index.tsx index 74a37e9734a..e941ad29099 100644 --- a/client/settings/fraud-protection/advanced-settings/index.tsx +++ b/client/settings/fraud-protection/advanced-settings/index.tsx @@ -24,7 +24,7 @@ import { useAdvancedFraudProtectionSettings, useSettings, } from '../../../data'; -import ErrorBoundary from '../../../components/error-boundary'; +import AdminErrorBoundary from 'wcpay/components/admin-error-boundary'; import { getAdminUrl } from '../../../utils'; import SettingsLayout from 'wcpay/settings/settings-layout'; import AVSMismatchRuleCard from './cards/avs-mismatch'; @@ -342,7 +342,7 @@ const FraudProtectionAdvancedSettingsPage: React.FC = () => { } } > - +
{ validationError && ( @@ -417,7 +417,7 @@ const FraudProtectionAdvancedSettingsPage: React.FC = () => { { renderSaveButton() }
-
+
diff --git a/client/settings/index.js b/client/settings/index.js index 5f7d3d9212d..a73ac89baf4 100644 --- a/client/settings/index.js +++ b/client/settings/index.js @@ -11,7 +11,7 @@ import enqueueFraudScripts from 'fraud-scripts'; import SettingsManager from 'settings/settings-manager'; import ExpressCheckoutSettings from './express-checkout-settings'; import WCPaySettingsContext from './wcpay-settings-context'; -import ErrorBoundary from '../components/error-boundary'; +import AdminErrorBoundary from '../components/admin-error-boundary'; window.addEventListener( 'load', () => { enqueueFraudScripts( wcpaySettings.fraudServices ); @@ -23,9 +23,9 @@ const settingsContainer = document.getElementById( if ( settingsContainer ) { ReactDOM.render( - + - + , settingsContainer ); @@ -39,9 +39,9 @@ if ( expressCheckoutSettingsContainer ) { ReactDOM.render( - + - + , expressCheckoutSettingsContainer ); diff --git a/client/settings/payment-methods-section/index.js b/client/settings/payment-methods-section/index.js index 48d39eb1633..b61fbf17d69 100644 --- a/client/settings/payment-methods-section/index.js +++ b/client/settings/payment-methods-section/index.js @@ -11,7 +11,7 @@ import { Card, CardHeader } from '@wordpress/components'; */ import SettingsSection from '../settings-section'; import LoadableSettingsSection from '../loadable-settings-section'; -import ErrorBoundary from '../../components/error-boundary'; +import AdminErrorBoundary from '../../components/admin-error-boundary'; import { useGetAvailablePaymentMethodIds } from '../../data'; import CardBody from 'wcpay/settings/card-body'; import PaymentMethodsList from '../payment-methods-list'; @@ -60,7 +60,7 @@ const PaymentMethodsSection = () => { id="payment-methods" > - +

@@ -81,7 +81,7 @@ const PaymentMethodsSection = () => { /> - + ); diff --git a/client/settings/settings-manager/index.js b/client/settings/settings-manager/index.js index 3da257c9074..7ca0b18f905 100644 --- a/client/settings/settings-manager/index.js +++ b/client/settings/settings-manager/index.js @@ -21,7 +21,7 @@ import Deposits from '../deposits'; import LoadableSettingsSection from '../loadable-settings-section'; import PaymentMethodsSection from '../payment-methods-section'; import BuyNowPayLaterSection from '../buy-now-pay-later-section'; -import ErrorBoundary from '../../components/error-boundary'; +import AdminErrorBoundary from '../../components/admin-error-boundary'; import { useDepositDelayDays, useGetDuplicatedPaymentMethodIds, @@ -186,9 +186,9 @@ const SettingsManager = () => { id="general" > - + - + { description={ ExpressCheckoutDescription } > - + - + @@ -216,21 +216,21 @@ const SettingsManager = () => { id="transactions" > - + - +
- + - +
@@ -239,9 +239,9 @@ const SettingsManager = () => { id="fp-settings" > - + - + { id="advanced-settings" > - + - + diff --git a/webpack/shared.js b/webpack/shared.js index 2dce99ca3ec..7cc1b7e1c60 100644 --- a/webpack/shared.js +++ b/webpack/shared.js @@ -1,7 +1,7 @@ /* eslint-disable */ const path = require( 'path' ); const { mapValues } = require( 'lodash' ); -const { ProvidePlugin } = require( 'webpack' ); +const { ProvidePlugin, DefinePlugin, webpack } = require( 'webpack' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); const WooCommerceDependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' ); const WebpackRTLPlugin = require( './webpack-rtl-plugin' ); @@ -162,6 +162,11 @@ module.exports = { } }, } ), + new DefinePlugin( { + 'process.env.MODE': JSON.stringify( + process.env.NODE_ENV || 'development' + ), + } ), ], resolveLoader: { modules: [