diff --git a/app/components/Views/ActivityView/index.js b/app/components/Views/ActivityView/index.js
index 40472acbda2..d4fdd786816 100644
--- a/app/components/Views/ActivityView/index.js
+++ b/app/components/Views/ActivityView/index.js
@@ -11,6 +11,7 @@ import TransactionsView from '../TransactionsView';
import TabBar from '../../Base/TabBar';
import { strings } from '../../../../locales/i18n';
import FiatOrdersView from '../FiatOrdersView';
+import ErrorBoundary from '../ErrorBoundary';
const styles = StyleSheet.create({
wrapper: {
@@ -30,12 +31,18 @@ function ActivityView({ hasOrders, ...props }) {
);
return (
-
-
-
- {hasOrders && }
-
-
+
+
+
+
+ {hasOrders && }
+
+
+
);
}
diff --git a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap
index b54bf94b161..1471bab6acd 100644
--- a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap
+++ b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap
@@ -1,321 +1,325 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Browser should render correctly 1`] = `
-
-
-
-
-
-
-
-
+ />
-
+
+
+
-
+
+
-
+
- Cancel
-
-
-
-
-
-
+ Cancel
+
+
+
+
+
+
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
`;
diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js
index a5067e7ef44..0e2c9ed3141 100644
--- a/app/components/Views/BrowserTab/index.js
+++ b/app/components/Views/BrowserTab/index.js
@@ -60,6 +60,7 @@ import { ethErrors } from 'eth-json-rpc-errors';
import EntryScriptWeb3 from '../../../core/EntryScriptWeb3';
import { getVersion } from 'react-native-device-info';
+import ErrorBoundary from '../ErrorBoundary';
const { HOMEPAGE_URL, USER_AGENT, NOTIFICATION_NAMES } = AppConstants;
const HOMEPAGE_HOST = 'home.metamask.io';
@@ -1679,42 +1680,44 @@ export const BrowserTab = props => {
* Main render
*/
return (
-
-
- {!!entryScriptWeb3 && firstUrlLoaded && (
- null} />}
- source={{ uri: initialUrl }}
- injectedJavaScriptBeforeContentLoaded={entryScriptWeb3}
- style={styles.webview}
- onLoadStart={onLoadStart}
- onLoadEnd={onLoadEnd}
- onLoadProgress={onLoadProgress}
- onMessage={onMessage}
- onError={onError}
- onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
- userAgent={USER_AGENT}
- sendCookies
- javascriptEnabled
- allowsInlineMediaPlayback
- useWebkit
- testID={'browser-webview'}
- />
- )}
+
+
+
+ {!!entryScriptWeb3 && firstUrlLoaded && (
+ null} />}
+ source={{ uri: initialUrl }}
+ injectedJavaScriptBeforeContentLoaded={entryScriptWeb3}
+ style={styles.webview}
+ onLoadStart={onLoadStart}
+ onLoadEnd={onLoadEnd}
+ onLoadProgress={onLoadProgress}
+ onMessage={onMessage}
+ onError={onError}
+ onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
+ userAgent={USER_AGENT}
+ sendCookies
+ javascriptEnabled
+ allowsInlineMediaPlayback
+ useWebkit
+ testID={'browser-webview'}
+ />
+ )}
+
+ {renderProgressBar()}
+ {isTabActive() && renderPhishingModal()}
+ {isTabActive() && renderUrlModal()}
+ {isTabActive() && renderApprovalModal()}
+ {isTabActive() && renderWatchAssetModal()}
+ {isTabActive() && renderOptions()}
+ {isTabActive() && renderBottomBar()}
+ {isTabActive() && renderOnboardingWizard()}
- {renderProgressBar()}
- {isTabActive() && renderPhishingModal()}
- {isTabActive() && renderUrlModal()}
- {isTabActive() && renderApprovalModal()}
- {isTabActive() && renderWatchAssetModal()}
- {isTabActive() && renderOptions()}
- {isTabActive() && renderBottomBar()}
- {isTabActive() && renderOnboardingWizard()}
-
+
);
};
diff --git a/app/components/Views/ErrorBoundary/index.js b/app/components/Views/ErrorBoundary/index.js
new file mode 100644
index 00000000000..890da5cc18c
--- /dev/null
+++ b/app/components/Views/ErrorBoundary/index.js
@@ -0,0 +1,222 @@
+import React, { Component } from 'react';
+import { SafeAreaView, Text, TouchableOpacity, View, StyleSheet, Image, Linking, Alert } from 'react-native';
+import PropTypes from 'prop-types';
+import RevealPrivateCredential from '../RevealPrivateCredential';
+import Logger from '../../../util/Logger';
+import { colors, fontStyles } from '../../../styles/common';
+import { ScrollView } from 'react-native-gesture-handler';
+import Clipboard from '@react-native-community/clipboard';
+import { strings } from '../../../../locales/i18n';
+import Icon from 'react-native-vector-icons/FontAwesome';
+
+// eslint-disable-next-line import/no-commonjs
+const metamaskErrorImage = require('../../../images/metamask-error.png');
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1
+ },
+ content: {
+ paddingHorizontal: 24,
+ flex: 1
+ },
+ header: {
+ alignItems: 'center'
+ },
+ errorImage: {
+ width: 50,
+ height: 50,
+ marginTop: 24
+ },
+ title: {
+ color: colors.black,
+ fontSize: 24,
+ lineHeight: 34,
+ ...fontStyles.bold
+ },
+ subtitle: {
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.grey500,
+ marginTop: 8,
+ textAlign: 'center',
+ ...fontStyles.normal
+ },
+ errorContainer: {
+ backgroundColor: colors.red000,
+ borderRadius: 8,
+ marginTop: 24
+ },
+ error: {
+ color: colors.black,
+ padding: 8,
+ fontSize: 14,
+ lineHeight: 20,
+ ...fontStyles.normal
+ },
+ button: {
+ marginTop: 24,
+ borderColor: colors.blue,
+ borderWidth: 1,
+ borderRadius: 50,
+ padding: 12,
+ paddingHorizontal: 34
+ },
+ buttonText: {
+ color: colors.blue,
+ textAlign: 'center',
+ ...fontStyles.normal,
+ fontWeight: '500'
+ },
+ textContainer: {
+ marginTop: 24
+ },
+ text: {
+ color: colors.black,
+ fontSize: 14,
+ lineHeight: 20,
+ ...fontStyles.normal
+ },
+ link: {
+ color: colors.blue
+ },
+ reportTextContainer: {
+ paddingLeft: 14,
+ marginTop: 16,
+ marginBottom: 24
+ },
+ reportStep: {
+ marginTop: 14
+ }
+});
+
+const Fallback = props => (
+
+
+
+
+ {strings('error_screen.title')}
+ {strings('error_screen.subtitle')}
+
+
+ {props.errorMessage}
+
+
+
+
+
+ {' '}
+ {strings('error_screen.try_again_button')}
+
+
+
+
+
+ {strings('error_screen.submit_ticket_1')}
+
+
+
+
+ {' '}
+ {strings('error_screen.submit_ticket_2')}
+
+
+
+
+ {' '}
+
+ {strings('error_screen.submit_ticket_3')}
+ {' '}
+ {strings('error_screen.submit_ticket_4')}
+
+
+
+
+ {' '}
+ {strings('error_screen.submit_ticket_5')}{' '}
+
+ {strings('error_screen.submit_ticket_6')}
+ {' '}
+ {strings('error_screen.submit_ticket_7')}
+
+
+
+ {strings('error_screen.save_seedphrase_1')}{' '}
+
+ {strings('error_screen.save_seedphrase_2')}
+ {' '}
+ {strings('error_screen.save_seedphrase_3')}
+
+
+
+
+);
+
+Fallback.propTypes = {
+ errorMessage: PropTypes.string,
+ resetError: PropTypes.func,
+ showExportSeedphrase: PropTypes.func,
+ copyErrorToClipboard: PropTypes.func,
+ openTicket: PropTypes.func
+};
+
+class ErrorBoundary extends Component {
+ state = { error: null };
+
+ static propTypes = {
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
+ view: PropTypes.string.isRequired
+ };
+
+ static getDerivedStateFromError(error) {
+ return { error };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ Logger.error(error, { View: this.props.view, ...errorInfo });
+ }
+
+ resetError = () => {
+ this.setState({ error: null });
+ };
+
+ showExportSeedphrase = () => {
+ this.setState({ backupSeedphrase: true });
+ };
+
+ cancelExportSeedphrase = () => {
+ this.setState({ backupSeedphrase: false });
+ };
+
+ getErrorMessage = () => `View: ${this.props.view}\n${this.state.error.toString()}`;
+
+ copyErrorToClipboard = async () => {
+ await Clipboard.setString(this.getErrorMessage());
+ Alert.alert(strings('error_screen.copied_clipboard'), '', [{ text: strings('error_screen.ok') }], {
+ cancelable: true
+ });
+ };
+
+ openTicket = () => {
+ const url = 'https://metamask.zendesk.com/hc/en-us/requests/new';
+ Linking.openURL(url);
+ };
+
+ render() {
+ return this.state.backupSeedphrase ? (
+
+ ) : this.state.error ? (
+
+ ) : (
+ this.props.children
+ );
+ }
+}
+
+export default ErrorBoundary;
diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js
index 279d75d6252..6cdc3006ccc 100644
--- a/app/components/Views/Login/index.js
+++ b/app/components/Views/Login/index.js
@@ -30,6 +30,7 @@ import {
ORIGINAL
} from '../../../constants/storage';
import { passwordRequirementsMet } from '../../../util/password';
+import ErrorBoundary from '../ErrorBoundary';
const styles = StyleSheet.create({
mainWrapper: {
@@ -335,78 +336,80 @@ class Login extends PureComponent {
};
render = () => (
-
-
-
-
- {Device.isAndroid() ? (
-
- ) : (
-
- )}
-
- {strings('login.title')}
-
- {strings('login.password')}
- (
-
+
+
+
+
+ {Device.isAndroid() ? (
+
+ ) : (
+
)}
- />
-
+
+ {strings('login.title')}
+
+ {strings('login.password')}
+ (
+
+ )}
+ />
+
- {this.renderSwitch()}
+ {this.renderSwitch()}
- {!!this.state.error && (
-
- {this.state.error}
-
- )}
+ {!!this.state.error && (
+
+ {this.state.error}
+
+ )}
-
-
- {this.state.loading ? (
-
- ) : (
- strings('login.login_button')
- )}
-
-
+
+
+ {this.state.loading ? (
+
+ ) : (
+ strings('login.login_button')
+ )}
+
+
-
-
+
+
+
-
-
-
-
+
+
+
+
);
}
diff --git a/app/components/Views/RevealPrivateCredential/index.js b/app/components/Views/RevealPrivateCredential/index.js
index 24ea18caa26..428c10a2a90 100644
--- a/app/components/Views/RevealPrivateCredential/index.js
+++ b/app/components/Views/RevealPrivateCredential/index.js
@@ -146,7 +146,6 @@ class RevealPrivateCredential extends PureComponent {
strings(`reveal_credential.${navigation.getParam('privateCredentialName', '')}_title`),
navigation
);
-
static propTypes = {
/**
/* navigation object required to push new views
@@ -163,7 +162,15 @@ class RevealPrivateCredential extends PureComponent {
/**
* Boolean that determines if the user has set a password before
*/
- passwordSet: PropTypes.bool
+ passwordSet: PropTypes.bool,
+ /**
+ * String that determines whether to show the seedphrase or private key export screen
+ */
+ privateCredentialName: PropTypes.string,
+ /**
+ * Cancel function to be called when cancel button is clicked. If not provided, we go to previous screen on cancel
+ */
+ cancel: PropTypes.func
};
async componentDidMount() {
@@ -193,20 +200,17 @@ class RevealPrivateCredential extends PureComponent {
};
cancel = () => {
+ if (this.props.cancel) return this.props.cancel();
const { navigation } = this.props;
navigation.pop();
};
async tryUnlockWithPassword(password) {
const { KeyringController } = Engine.context;
- const {
- selectedAddress,
- navigation: {
- state: {
- params: { privateCredentialName }
- }
- }
- } = this.props;
+ const { selectedAddress } = this.props;
+
+ const privateCredentialName =
+ this.props.privateCredentialName || this.props.navigation.state.params.privateCredentialName;
try {
if (privateCredentialName === 'seed_phrase') {
@@ -241,13 +245,9 @@ class RevealPrivateCredential extends PureComponent {
copyPrivateCredentialToClipboard = async () => {
const { privateCredential } = this.state;
- const {
- navigation: {
- state: {
- params: { privateCredentialName }
- }
- }
- } = this.props;
+ const privateCredentialName =
+ this.props.privateCredentialName || this.props.navigation.state.params.privateCredentialName;
+
await Clipboard.setString(privateCredential);
this.props.showAlert({
isVisible: true,
@@ -272,13 +272,9 @@ class RevealPrivateCredential extends PureComponent {
render = () => {
const { unlocked, privateCredential } = this.state;
- const {
- navigation: {
- state: {
- params: { privateCredentialName }
- }
- }
- } = this.props;
+ const privateCredentialName =
+ this.props.privateCredentialName || this.props.navigation.state.params.privateCredentialName;
+
return (
-
+
+
+
`;
diff --git a/app/components/Views/Root/index.js b/app/components/Views/Root/index.js
index 97df4fda47b..f20dbd4d001 100644
--- a/app/components/Views/Root/index.js
+++ b/app/components/Views/Root/index.js
@@ -9,6 +9,8 @@ import SplashScreen from 'react-native-splash-screen';
import App from '../../Nav/App';
import SecureKeychain from '../../../core/SecureKeychain';
import EntryScriptWeb3 from '../../../core/EntryScriptWeb3';
+import Logger from '../../../util/Logger';
+import ErrorBoundary from '../ErrorBoundary';
/**
* Top level of the component hierarchy
@@ -23,6 +25,10 @@ export default class Root extends PureComponent {
foxCode: 'null'
};
+ errorHandler = (error, stackTrace) => {
+ Logger.error(error, stackTrace);
+ };
+
constructor(props) {
super(props);
SecureKeychain.init(props.foxCode);
@@ -34,7 +40,9 @@ export default class Root extends PureComponent {
render = () => (
-
+
+
+
);
diff --git a/app/components/Views/Wallet/index.js b/app/components/Views/Wallet/index.js
index 6c36aa4afde..0b94f0f6ac7 100644
--- a/app/components/Views/Wallet/index.js
+++ b/app/components/Views/Wallet/index.js
@@ -18,6 +18,7 @@ import { getTicker } from '../../../util/transactions';
import OnboardingWizard from '../../UI/OnboardingWizard';
import { showTransactionNotification, hideTransactionNotification } from '../../../actions/notification';
import DeeplinkManager from '../../../core/DeeplinkManager';
+import ErrorBoundary from '../ErrorBoundary';
const styles = StyleSheet.create({
wrapper: {
@@ -238,15 +239,17 @@ class Wallet extends PureComponent {
};
render = () => (
-
- }
- >
- {this.props.selectedAddress ? this.renderContent() : this.renderLoader()}
-
- {this.renderOnboardingWizard()}
-
+
+
+ }
+ >
+ {this.props.selectedAddress ? this.renderContent() : this.renderLoader()}
+
+ {this.renderOnboardingWizard()}
+
+
);
}
diff --git a/app/images/metamask-error.png b/app/images/metamask-error.png
new file mode 100644
index 00000000000..31eac551949
Binary files /dev/null and b/app/images/metamask-error.png differ
diff --git a/locales/en.json b/locales/en.json
index c152051e4df..c2640b4a886 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1325,5 +1325,22 @@
"text": "Don’t risk losing your funds. Protect your wallet by saving your seed phrase in a place you trust.",
"text_bold": "It’s the only way to recover your wallet if you get locked out of the app or get a new device.",
"action": "Learn more"
+ },
+ "error_screen": {
+ "title": "An error occured",
+ "subtitle": "Your information can't be shown. Don’t worry, your wallet and funds are safe.",
+ "try_again_button": "Try again",
+ "submit_ticket_1": "Please report this issue so we can fix it:",
+ "submit_ticket_2": "Take a screenshot of this screen.",
+ "submit_ticket_3": "Copy",
+ "submit_ticket_4": "the error message to clipboard.",
+ "submit_ticket_5": "Submit a ticket",
+ "submit_ticket_6": "here.",
+ "submit_ticket_7": "Please include the error message and the screenshot.",
+ "save_seedphrase_1": "If this error persists,",
+ "save_seedphrase_2": "save your seed phrase",
+ "save_seedphrase_3": "& re-install the app. Note: you can NOT restore your wallet without your seed phrase.",
+ "copied_clipboard": "Copied to clipboard",
+ "ok": "OK"
}
}