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:
parent
c9c2f6a366
commit
91cfad0c0e
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,4 +22,4 @@ yarn-error.log*
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
_book
|
_book
|
||||||
docs/html
|
docs/html
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
74
packages/extension-ui/src/components/ErrorBoundary.tsx
Normal file
74
packages/extension-ui/src/components/ErrorBoundary.tsx
Normal 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);
|
||||||
@ -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';
|
||||||
|
|||||||
10
packages/extension-ui/src/components/translate.ts
Normal file
10
packages/extension-ui/src/components/translate.ts
Normal 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();
|
||||||
@ -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';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user