Browse Source

Add ErrorBoundary component (#2026)

* Add ErrorBoundary component

* Decode call via effect

* Cleanups
Jaco Greeff 5 years ago
parent
commit
406a505871

+ 34 - 15
packages/react-components/src/Call.tsx

@@ -2,11 +2,12 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { Hash } from '@polkadot/types/interfaces';
 import { Codec, IExtrinsic, IMethod, TypeDef } from '@polkadot/types/types';
 import { BareProps, I18nProps } from './types';
 
 import BN from 'bn.js';
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import styled from 'styled-components';
 import { GenericCall, getTypeDef } from '@polkadot/types';
 import Params from '@polkadot/react-params';
@@ -19,24 +20,41 @@ import translate from './translate';
 export interface Props extends I18nProps, BareProps {
   children?: React.ReactNode;
   labelHash?: React.ReactNode;
+  mortality?: string;
+  onError?: () => void;
   value: IExtrinsic | IMethod;
   withHash?: boolean;
-  mortality?: string;
   tip?: BN;
 }
 
-function Call ({ children, className, labelHash, style, mortality, tip, value, withHash, t }: Props): React.ReactElement<Props> {
-  const params = GenericCall.filterOrigin(value.meta).map(({ name, type }): { name: string; type: TypeDef } => ({
-    name: name.toString(),
-    type: getTypeDef(type.toString())
-  }));
-  const values = value.args.map((value): { isValid: boolean; value: Codec } => ({
-    isValid: true,
-    value
-  }));
-  const hash = withHash
-    ? (value as IExtrinsic).hash
-    : null;
+interface Param {
+  name: string;
+  type: TypeDef;
+}
+
+interface Value {
+  isValid: boolean;
+  value: Codec;
+}
+
+function Call ({ children, className, labelHash, mortality, onError, style, tip, value, withHash, t }: Props): React.ReactElement<Props> {
+  const [{ hash, params, values }, setExtracted] = useState<{ hash: Hash | null; params: Param[]; values: Value[] }>({ hash: null, params: [], values: [] });
+
+  useEffect((): void => {
+    const params = GenericCall.filterOrigin(value.meta).map(({ name, type }): Param => ({
+      name: name.toString(),
+      type: getTypeDef(type.toString())
+    }));
+    const values = value.args.map((value): Value => ({
+      isValid: true,
+      value
+    }));
+    const hash = withHash
+      ? value.hash
+      : null;
+
+    setExtracted({ hash, params, values });
+  }, [value, withHash]);
 
   return (
     <div
@@ -45,6 +63,7 @@ function Call ({ children, className, labelHash, style, mortality, tip, value, w
     >
       <Params
         isDisabled
+        onError={onError}
         params={params}
         values={values}
       />
@@ -66,7 +85,7 @@ function Call ({ children, className, labelHash, style, mortality, tip, value, w
             {mortality}
           </Static>
         )}
-        {tip && tip.gtn(0) && (
+        {tip?.gtn(0) && (
           <Static
             className='tip'
             label={t('tip')}

+ 55 - 0
packages/react-components/src/ErrorBoundary.tsx

@@ -0,0 +1,55 @@
+// Copyright 2017-2019 @polkadot/react-components authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from './types';
+
+import React from 'react';
+
+import translate from './translate';
+
+interface Props extends I18nProps {
+  children: React.ReactNode;
+  doThrow?: boolean;
+  onError?: () => void;
+}
+
+interface State {
+  hasError: boolean;
+}
+
+class ErrorBoundary extends React.Component<Props> {
+  state: State = { hasError: false };
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  static getDerivedStateFromError (_error: Error): State {
+    // Update state so the next render will show the fallback UI.
+    return { hasError: true };
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  public componentDidCatch (error: Error, _errorInfo: any): void {
+    const { doThrow, onError } = this.props;
+
+    onError && onError();
+
+    if (doThrow) {
+      throw error;
+    }
+  }
+
+  public render (): React.ReactNode {
+    const { children, t } = this.props;
+    const { hasError } = this.state;
+
+    return hasError
+      ? (
+        <article className='error'>
+          {t('Uncaught error. Something went wrong with the data extraction, resulting in an error log.')}
+        </article>
+      )
+      : children;
+  }
+}
+
+export default translate(ErrorBoundary);

+ 1 - 0
packages/react-components/src/index.tsx

@@ -28,6 +28,7 @@ export { default as CopyButton } from './CopyButton';
 export { default as CryptoType } from './CryptoType';
 export { default as Dropdown } from './Dropdown';
 export { default as Editor } from './Editor';
+export { default as ErrorBoundary } from './ErrorBoundary';
 export { default as Event } from './Event';
 export { default as Expander } from './Expander';
 export { default as Extrinsic } from './Extrinsic';

+ 15 - 33
packages/react-params/src/Param/Proposal.tsx

@@ -23,37 +23,19 @@ export default function Proposal (props: Props): React.ReactElement<Props> {
   }
 
   const proposal = value as Extrinsic;
-
-  try {
-    const { method, section } = registry.findMetaCall(proposal.callIndex);
-
-    return (
-      <Bare>
-        <Static
-          className={classes(className, 'full')}
-          label={label}
-          style={style}
-          withLabel={withLabel}
-        >
-          {section}.{method}
-        </Static>
-        <Call value={proposal} />
-      </Bare>
-    );
-  } catch (error) {
-    console.error(error);
-
-    return (
-      <Bare>
-        <Static
-          className={classes(className, 'full')}
-          label={label}
-          style={style}
-          withLabel={withLabel}
-        >
-          Unable to render extrinsic.
-        </Static>
-      </Bare>
-    );
-  }
+  const { method, section } = registry.findMetaCall(proposal.callIndex);
+
+  return (
+    <Bare>
+      <Static
+        className={classes(className, 'full')}
+        label={label}
+        style={style}
+        withLabel={withLabel}
+      >
+        {section}.{method}
+      </Static>
+      <Call value={proposal} />
+    </Bare>
+  );
 }

+ 26 - 16
packages/react-params/src/index.tsx

@@ -9,6 +9,7 @@ import './Params.css';
 
 import React from 'react';
 import styled from 'styled-components';
+import { ErrorBoundary } from '@polkadot/react-components';
 import { classes } from '@polkadot/react-components/util';
 
 import ParamComp from './ParamComp';
@@ -19,6 +20,7 @@ interface Props extends I18nProps {
   isDisabled?: boolean;
   onChange?: (value: RawParams) => void;
   onEnter?: () => void;
+  onError?: () => void;
   onEscape?: () => void;
   overrides?: ComponentMap;
   params: ParamDef[];
@@ -85,22 +87,24 @@ class Params extends React.PureComponent<Props, State> {
         className={classes('ui--Params', className)}
         style={style}
       >
-        <div className='ui--Params-Content'>
-          {values && params.map(({ name, type }: ParamDef, index: number): React.ReactNode => (
-            <ParamComp
-              defaultValue={values[index]}
-              index={index}
-              isDisabled={isDisabled}
-              key={`${name}:${type}:${index}`}
-              name={name}
-              onChange={this.onChangeParam}
-              onEnter={onEnter}
-              onEscape={onEscape}
-              overrides={overrides}
-              type={type}
-            />
-          ))}
-        </div>
+        <ErrorBoundary onError={this.onRenderError}>
+          <div className='ui--Params-Content'>
+            {values && params.map(({ name, type }: ParamDef, index: number): React.ReactNode => (
+              <ParamComp
+                defaultValue={values[index]}
+                index={index}
+                isDisabled={isDisabled}
+                key={`${name}:${type}:${index}`}
+                name={name}
+                onChange={this.onChangeParam}
+                onEnter={onEnter}
+                onEscape={onEscape}
+                overrides={overrides}
+                type={type}
+              />
+            ))}
+          </div>
+        </ErrorBoundary>
       </div>
     );
   }
@@ -136,6 +140,12 @@ class Params extends React.PureComponent<Props, State> {
 
     onChange && onChange(values);
   }
+
+  private onRenderError = (): void => {
+    const { onError } = this.props;
+
+    onError && onError();
+  }
 }
 
 export default translate(

+ 14 - 5
packages/react-signer/src/Modal.tsx

@@ -17,7 +17,7 @@ import React from 'react';
 import { SubmittableResult } from '@polkadot/api';
 import { web3FromSource } from '@polkadot/extension-dapp';
 import { createType } from '@polkadot/types';
-import { Button, InputBalance, Modal, Toggle } from '@polkadot/react-components';
+import { Button, InputBalance, Modal, Toggle, ErrorBoundary } from '@polkadot/react-components';
 import { registry, withApi, withMulti, withObservable } from '@polkadot/react-api';
 import keyring from '@polkadot/ui-keyring';
 import { assert, isFunction } from '@polkadot/util';
@@ -43,6 +43,7 @@ interface State {
   currentItem?: QueueTx;
   isQrScanning: boolean;
   isQrVisible: boolean;
+  isRenderError: boolean;
   isSendable: boolean;
   isV2?: boolean;
   password: string;
@@ -94,9 +95,10 @@ async function makeExtrinsicSignature (payload: SignerPayloadJSON, { id, signerC
 
 class Signer extends React.PureComponent<Props, State> {
   public state: State = {
-    isSendable: false,
     isQrScanning: false,
     isQrVisible: false,
+    isRenderError: false,
+    isSendable: false,
     password: '',
     qrAddress: '',
     qrPayload: new Uint8Array(),
@@ -164,7 +166,9 @@ class Signer extends React.PureComponent<Props, State> {
         className='ui--signer-Signer'
         open
       >
-        {this.renderContent()}
+        <ErrorBoundary onError={this.onRenderError}>
+          {this.renderContent()}
+        </ErrorBoundary>
         {this.renderButtons()}
       </Modal>
     );
@@ -172,7 +176,7 @@ class Signer extends React.PureComponent<Props, State> {
 
   private renderButtons (): React.ReactNode {
     const { t } = this.props;
-    const { currentItem, isQrScanning, isQrVisible, isSendable } = this.state;
+    const { currentItem, isQrScanning, isQrVisible, isRenderError, isSendable } = this.state;
 
     if (!currentItem) {
       return null;
@@ -194,7 +198,7 @@ class Signer extends React.PureComponent<Props, State> {
             label={t('Cancel')}
             icon='cancel'
           />
-          {(!isQrVisible || !isQrScanning) && (
+          {!isRenderError && (!isQrVisible || !isQrScanning) && (
             <>
               <Button.Or />
               <Button
@@ -246,6 +250,7 @@ class Signer extends React.PureComponent<Props, State> {
       <Transaction
         hideDetails={isQrVisible}
         isSendable={isSendable}
+        onError={this.onRenderError}
         tip={tip}
         value={currentItem}
       >
@@ -302,6 +307,10 @@ class Signer extends React.PureComponent<Props, State> {
     );
   }
 
+  private onRenderError = (): void => {
+    this.setState({ isRenderError: true });
+  }
+
   private onShowTip = (showTip: boolean): void => {
     this.setState({ showTip });
   }

+ 40 - 49
packages/react-signer/src/Transaction.tsx

@@ -17,64 +17,55 @@ interface Props extends I18nProps {
   children?: React.ReactNode;
   hideDetails?: boolean;
   isSendable: boolean;
+  onError: () => void;
   tip?: BN;
   value: QueueTx;
 }
 
-function Transaction ({ children, hideDetails, isSendable, value: { accountId, extrinsic, isUnsigned }, t, tip }: Props): React.ReactElement<Props> | null {
+function Transaction ({ children, hideDetails, isSendable, onError, value: { accountId, extrinsic, isUnsigned }, t, tip }: Props): React.ReactElement<Props> | null {
   if (!extrinsic) {
     return null;
   }
 
-  try {
-    const { meta, method, section } = registry.findMetaCall(extrinsic.callIndex);
+  const { meta, method, section } = registry.findMetaCall(extrinsic.callIndex);
 
-    return (
-      <>
-        <Modal.Header>
-          {section}.{method}
-          <label><details><summary>{meta?.documentation.join(' ') || t('Details')}</summary></details></label>
-        </Modal.Header>
-        <Modal.Content className='ui--signer-Signer-Content'>
-          {!hideDetails && (
-            <>
-              {!isUnsigned && accountId && (
-                <InputAddress
-                  className='full'
-                  defaultValue={accountId}
-                  isDisabled
-                  isInput
-                  label={t('sending from my account')}
-                  withLabel
-                />
-              )}
-              <Call value={extrinsic} />
-              {!isUnsigned && (
-                <Checks
-                  accountId={accountId}
-                  extrinsic={extrinsic}
-                  isSendable={isSendable}
-                  tip={tip}
-                />
-              )}
-            </>
-          )}
-          {children}
-        </Modal.Content>
-      </>
-    );
-  } catch (error) {
-    console.error(error);
-
-    return (
-      <>
-        <Modal.Header>{t('FATAL')}</Modal.Header>
-        <Modal.Content className='ui--signer-Signer-Content'>
-          {t('Unable to render extrinsic, invalid')}
-        </Modal.Content>
-      </>
-    );
-  }
+  return (
+    <>
+      <Modal.Header>
+        {section}.{method}
+        <label><details><summary>{meta?.documentation.join(' ') || t('Details')}</summary></details></label>
+      </Modal.Header>
+      <Modal.Content className='ui--signer-Signer-Content'>
+        {!hideDetails && (
+          <>
+            {!isUnsigned && accountId && (
+              <InputAddress
+                className='full'
+                defaultValue={accountId}
+                isDisabled
+                isInput
+                label={t('sending from my account')}
+                withLabel
+              />
+            )}
+            <Call
+              onError={onError}
+              value={extrinsic}
+            />
+            {!isUnsigned && (
+              <Checks
+                accountId={accountId}
+                extrinsic={extrinsic}
+                isSendable={isSendable}
+                tip={tip}
+              />
+            )}
+          </>
+        )}
+        {children}
+      </Modal.Content>
+    </>
+  );
 }
 
 export default translate(Transaction);