Add error boundaries with fallback component to show any error (#471)

* add error boundaries with fallback component

* use own ErrorBoundaries

* fix build

* simplistic implementation

* Update packages/extension-ui/src/components/ErrorBoundary.tsx

* keep fragments

* at the root only

* weird svg error

* double click needed

* remove trigger

* demove errorDisplay and move goHome to class

* cleanup and button

* Update packages/extension-ui/src/Popup/ImportSeed.tsx

* use trigger

* remove intentionnal error

* Update packages/extension-ui/src/Popup/index.tsx

* Update packages/extension-ui/src/components/ErrorBoundary.tsx

* fix build

* Update packages/extension-ui/src/Popup/index.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Thibaut Sardan 2020-10-10 10:42:35 +02:00 committed by GitHub
parent c9c2f6a366
commit 91cfad0c0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 107 additions and 17 deletions

2
.gitignore vendored
View File

@ -22,4 +22,4 @@ yarn-error.log*
package-lock.json package-lock.json
_book _book
docs/html docs/html
.idea .idea

View File

@ -9,7 +9,7 @@ import { Route, Switch } from 'react-router';
import uiSettings from '@polkadot/ui-settings'; import uiSettings from '@polkadot/ui-settings';
import { setSS58Format } from '@polkadot/util-crypto'; import { setSS58Format } from '@polkadot/util-crypto';
import { Loading } from '../components'; import { Loading, ErrorBoundary } from '../components';
import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SettingsContext, SigningReqContext } from '../components/contexts'; import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SettingsContext, SigningReqContext } from '../components/contexts';
import ToastProvider from '../components/Toast/ToastProvider'; import ToastProvider from '../components/Toast/ToastProvider';
import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging'; import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging';
@ -103,15 +103,19 @@ export default function Popup (): React.ReactElement {
.catch(console.error); .catch(console.error);
}, [cameraOn]); }, [cameraOn]);
function wrapWithErrorBoundary (component: React.ReactElement, trigger?: string): React.ReactElement {
return <ErrorBoundary trigger={trigger}>{component}</ErrorBoundary>;
}
const Root = isWelcomeDone const Root = isWelcomeDone
? authRequests && authRequests.length ? authRequests && authRequests.length
? Authorize ? wrapWithErrorBoundary(<Authorize />, 'authorize')
: metaRequests && metaRequests.length : metaRequests && metaRequests.length
? Metadata ? wrapWithErrorBoundary(<Metadata />, 'metadata')
: signRequests && signRequests.length : signRequests && signRequests.length
? Signing ? wrapWithErrorBoundary(<Signing/>, 'signing')
: Accounts : wrapWithErrorBoundary(<Accounts/>, 'accounts')
: Welcome; : wrapWithErrorBoundary(<Welcome />, 'welcome');
return ( return (
<Loading>{accounts && authRequests && metaRequests && signRequests && ( <Loading>{accounts && authRequests && metaRequests && signRequests && (
@ -124,19 +128,19 @@ export default function Popup (): React.ReactElement {
<SigningReqContext.Provider value={signRequests}> <SigningReqContext.Provider value={signRequests}>
<ToastProvider> <ToastProvider>
<Switch> <Switch>
<Route path='/account/create'><CreateAccount /></Route> <Route path='/account/create'>{wrapWithErrorBoundary(<CreateAccount />, 'account-creation')}</Route>
<Route path='/account/forget/:address'><Forget /></Route> <Route path='/account/forget/:address'>{wrapWithErrorBoundary(<Forget />, 'forget-address')}</Route>
<Route path='/account/export/:address'><Export /></Route> <Route path='/account/export/:address'>{wrapWithErrorBoundary(<Export />, 'export-address')}</Route>
<Route path='/account/import-qr'><ImportQr /></Route> <Route path='/account/import-qr'>{wrapWithErrorBoundary(<ImportQr />, 'import-qr')}</Route>
<Route path='/account/import-seed'><ImportSeed /></Route> <Route path='/account/import-seed'>{wrapWithErrorBoundary(<ImportSeed />, 'import-seed')}</Route>
<Route path='/account/restore-json'><RestoreJson /></Route> <Route path='/account/restore-json'>{wrapWithErrorBoundary(<RestoreJson />, 'restore-json')}</Route>
<Route path='/account/derive/:address/locked'><Derive isLocked /></Route> <Route path='/account/derive/:address/locked'>{wrapWithErrorBoundary(<Derive isLocked />, 'derived-address-locked')}</Route>
<Route path='/account/derive/:address'><Derive /></Route> <Route path='/account/derive/:address'>{wrapWithErrorBoundary(<Derive />, 'derive-address')}</Route>
<Route <Route
exact exact
path='/' path='/'
> >
<Root /> {Root}
</Route> </Route>
</Switch> </Switch>
</ToastProvider> </ToastProvider>

View File

@ -0,0 +1,74 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributor
// SPDX-License-Identifier: Apache-2.0
import { WithTranslation } from 'react-i18next';
import React from 'react';
import ButtonArea from './ButtonArea';
import Button from './Button';
import VerticalSpace from './VerticalSpace';
import Header from '../partials/Header';
import translate from './translate';
interface Props extends WithTranslation {
children: React.ReactNode;
className?: string;
error?: Error | null;
trigger?: string;
}
interface State {
error: Error | null;
}
// NOTE: This is the only way to do an error boundary, via extend
class ErrorBoundary extends React.Component<Props> {
state: State = { error: null };
static getDerivedStateFromError (error: Error): Partial<State> {
return { error };
}
componentDidUpdate (prevProps: Props) {
const { error } = this.state;
const { trigger } = this.props;
if (error !== null && (prevProps.trigger !== trigger)) {
this.setState({ error: null });
}
}
goHome = () => {
this.setState({ error: null });
window.location.hash = '/';
};
public render (): React.ReactNode {
const { children, t } = this.props;
const { error } = this.state;
return error
? (
<>
<Header text={t<string>('An error occured')} />
<div>
{t<string>('Something went wrong with the query and rendering of this component. {{message}}', {
replace: { message: error.message }
})}
</div>
<VerticalSpace/>
<ButtonArea>
<Button
onClick={this.goHome}
>
{t<string>('Back to home')}
</Button>
</ButtonArea>
</>
)
: children;
}
}
export default translate(ErrorBoundary);

View File

@ -11,6 +11,7 @@ export { default as ButtonArea } from './ButtonArea';
export { default as ButtonWithSubtitle } from './ButtonWithSubtitle'; export { default as ButtonWithSubtitle } from './ButtonWithSubtitle';
export { default as Checkbox } from './Checkbox'; export { default as Checkbox } from './Checkbox';
export { default as Dropdown } from './Dropdown'; export { default as Dropdown } from './Dropdown';
export { default as ErrorBoundary } from './ErrorBoundary';
export { default as Fonts } from './Fonts'; export { default as Fonts } from './Fonts';
export { default as Icon } from './Icon'; export { default as Icon } from './Icon';
export { default as Identicon } from './Identicon'; export { default as Identicon } from './Identicon';

View File

@ -0,0 +1,10 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useTranslation as useTranslationBase, UseTranslationResponse, withTranslation } from 'react-i18next';
export function useTranslation (): UseTranslationResponse {
return useTranslationBase();
}
export default withTranslation();

View File

@ -10,7 +10,8 @@ import ArrowLeftImage from '../assets/arrowLeft.svg';
import gear from '../assets/gear.svg'; import gear from '../assets/gear.svg';
import plus from '../assets/plus.svg'; import plus from '../assets/plus.svg';
import logo from '../assets/pjs.svg'; import logo from '../assets/pjs.svg';
import { Link, Svg } from '../components'; import Link from '../components/Link';
import Svg from '../components/Svg';
import useOutsideClick from '../hooks/useOutsideClick'; import useOutsideClick from '../hooks/useOutsideClick';
import MenuAdd from './MenuAdd'; import MenuAdd from './MenuAdd';
import MenuSettings from './MenuSettings'; import MenuSettings from './MenuSettings';