diff --git a/packages/client/src/v2-events/routes/TRPCErrorBoundary.stories.tsx b/packages/client/src/v2-events/routes/TRPCErrorBoundary.stories.tsx new file mode 100644 index 0000000000..36c5b8a163 --- /dev/null +++ b/packages/client/src/v2-events/routes/TRPCErrorBoundary.stories.tsx @@ -0,0 +1,71 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ + +import React from 'react' +import { Meta, StoryFn } from '@storybook/react' +import { TRPCClientError } from '@trpc/client' +import { TRPCProvider } from '@client/v2-events/trpc' +import { TRPCErrorBoundary } from '@client/v2-events/routes/TRPCErrorBoundary' + +const meta: Meta = { + title: 'Components/TRPCErrorBoundary', + decorators: [ + (Story) => ( + + + + + + ) + ] +} + +export default meta + +// Default story: No error, renders children normally +export const Default: StoryFn = () => ( + +
+

{'Normal Render'}

+

+ {'This content is inside the error boundary and renders correctly.'} +

+
+
+) + +// Story that simulates an error +export const WithError: StoryFn = () => ( + + {(() => { + throw new Error('This is a test error for TRPCErrorBoundary.') + })()} + +) + +// Story for 401 Unauthorized Error +export const UnauthorizedError: StoryFn = () => ( + + {(() => { + ;(() => { + const error = new TRPCClientError('Unauthorized', { + meta: { + response: { + status: 401, + statusText: 'Unauthorized' + } + } + }) + throw error + })() + })()} + +) diff --git a/packages/client/src/v2-events/routes/TRPCErrorBoundary.tsx b/packages/client/src/v2-events/routes/TRPCErrorBoundary.tsx new file mode 100644 index 0000000000..34fc840878 --- /dev/null +++ b/packages/client/src/v2-events/routes/TRPCErrorBoundary.tsx @@ -0,0 +1,150 @@ +/* eslint-disable react/destructuring-assignment */ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * OpenCRVS is also distributed under the terms of the Civil Registration + * & Healthcare Disclaimer located at http://opencrvs.org/license. + * + * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. + */ +import React, { Component } from 'react' +import * as Sentry from '@sentry/react' +import styled from 'styled-components' +import { TRPCClientError } from '@trpc/client' +import { connect } from 'react-redux' +import { injectIntl, WrappedComponentProps as IntlShapeProps } from 'react-intl' +import { PageWrapper } from '@opencrvs/components/lib/PageWrapper' +import { TertiaryButton } from '@opencrvs/components/lib/buttons' +import { Box } from '@opencrvs/components/lib/Box' +import { errorMessages, buttonMessages } from '@client/i18n/messages' +// eslint-disable-next-line no-restricted-imports +import { redirectToAuthentication } from '@client/profile/profileActions' + +const ErrorContainer = styled(Box)` + display: flex; + width: 400px; + flex-direction: column; + align-items: center; + margin-top: -80px; +` +const ErrorTitle = styled.h1` + ${({ theme }) => theme.fonts.h1}; + color: ${({ theme }) => theme.colors.copy}; + margin-bottom: 16px; +` + +const ErrorMessage = styled.div` + ${({ theme }) => theme.fonts.reg18}; + color: ${({ theme }) => theme.colors.copy}; + margin-bottom: 32px; + text-align: center; +` + +const development = ['127.0.0.1', 'localhost'].includes( + window.location.hostname +) + +interface Props extends IntlShapeProps { + children: React.ReactNode + redirectToAuthentication: typeof redirectToAuthentication +} + +interface State { + error: Error | null +} + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { error: null } + } + + static getDerivedStateFromError(error: Error) { + return { error } + } + + componentDidCatch(error: Error) { + // eslint-disable-next-line no-console + console.error('TRPC Error Caught:', error) + } + + render() { + // eslint-disable-next-line no-shadow + const { intl, redirectToAuthentication } = this.props + if (this.state.error) { + const error = this.state.error + let httpCode = 500 + let message = error.message + + if (error instanceof TRPCClientError) { + if ( + error.meta && + typeof error.meta === 'object' && + 'response' in error.meta && + error.meta.response && + typeof error.meta.response === 'object' && + 'status' in error.meta.response && + 'statusText' in error.meta.response + ) { + httpCode = Number(error.meta.response.status) + message = String(error.meta.response.statusText) + } + } + /** + * TODO: Improve the error message design once the probable errors are defined + * and the design/ux is ready. + */ + return ( + { + // eslint-disable-next-line no-console + console.log('Sentry.ErrorBoundary: ', err) + }} + > + + + {httpCode === 401 ? ( + <> + + {intl.formatMessage(errorMessages.errorTitleUnauthorized)} + + + {intl.formatMessage(errorMessages.errorCodeUnauthorized)} + + redirectToAuthentication(true)} + > + {intl.formatMessage(buttonMessages.login)} + + + ) : ( + <> + + {intl.formatMessage(errorMessages.errorTitle)} + + {message} + (window.location.href = '/')} + > + {intl.formatMessage(buttonMessages.goToHomepage)} + + + )} + + + + ) + } + + return this.props.children + } +} + +export const TRPCErrorBoundary = connect(null, { redirectToAuthentication })( + injectIntl(ErrorBoundary) +) diff --git a/packages/client/src/v2-events/routes/config.tsx b/packages/client/src/v2-events/routes/config.tsx index fbbaf8304d..bc0cbd630a 100644 --- a/packages/client/src/v2-events/routes/config.tsx +++ b/packages/client/src/v2-events/routes/config.tsx @@ -11,6 +11,7 @@ import React from 'react' import { Outlet, RouteObject } from 'react-router-dom' + import { Debug } from '@client/v2-events/features/debug/debug' import { router as correctionRouter } from '@client/v2-events/features/events/actions/correct/request/router' import * as Declare from '@client/v2-events/features/events/actions/declare' @@ -24,6 +25,7 @@ import { router as workqueueRouter } from '@client/v2-events/features/workqueues import { EventOverviewLayout } from '@client/v2-events/layouts' import { TRPCProvider } from '@client/v2-events/trpc' import AdvancedSearch from '@client/v2-events/features/events/AdvancedSearch/AdvancedSearch' +import { TRPCErrorBoundary } from '@client/v2-events/routes/TRPCErrorBoundary' import { ROUTES } from './routes' /** @@ -35,10 +37,12 @@ import { ROUTES } from './routes' export const routesConfig = { path: ROUTES.V2.path, element: ( - - - - + + + + + + ), children: [ workqueueRouter,