diff --git a/src/App.js b/src/App.js index 58845d56e429..c69e6da22cb8 100644 --- a/src/App.js +++ b/src/App.js @@ -1,12 +1,15 @@ import React from 'react'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import CustomStatusBar from './components/CustomStatusBar'; +import ErrorBoundary from './components/ErrorBoundary'; import Expensify from './Expensify'; const App = () => ( - + + + ); diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.js b/src/components/ErrorBoundary/BaseErrorBoundary.js new file mode 100644 index 000000000000..aee0f94a5665 --- /dev/null +++ b/src/components/ErrorBoundary/BaseErrorBoundary.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + /* A message posted to `logError` (along with error data) when this component intercepts an error */ + errorMessage: PropTypes.string.isRequired, + + /* A function to perform the actual logging since different platforms support different tools */ + logError: PropTypes.func, + + /* Actual content wrapped by this error boundary */ + children: PropTypes.node.isRequired, +}; + +const defaultProps = { + logError: () => {}, +}; + +/** + * This component captures an error in the child component tree and logs it to the server + * It can be used to wrap the entire app as well as to wrap specific parts for more granularity + * @see {@link https://reactjs.org/docs/error-boundaries.html#where-to-place-error-boundaries} + */ +class BaseErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = {hasError: false}; + } + + static getDerivedStateFromError() { + // Update state so the next render will show the fallback UI. + return {hasError: true}; + } + + componentDidCatch(error, errorInfo) { + this.props.logError(this.props.errorMessage, error, errorInfo); + } + + render() { + if (this.state.hasError) { + // For the moment we've decided not to render any fallback UI + return null; + } + + return this.props.children; + } +} + +BaseErrorBoundary.propTypes = propTypes; +BaseErrorBoundary.defaultProps = defaultProps; + +export default BaseErrorBoundary; diff --git a/src/components/ErrorBoundary/index.js b/src/components/ErrorBoundary/index.js new file mode 100644 index 000000000000..7e8fdfdc2131 --- /dev/null +++ b/src/components/ErrorBoundary/index.js @@ -0,0 +1,9 @@ +import BaseErrorBoundary from './BaseErrorBoundary'; +import Log from '../../libs/Log'; + +BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => { + // Log the error to the server + Log.alert(errorMessage, 0, {error: error.message, errorInfo}, false); +}; + +export default BaseErrorBoundary; diff --git a/src/components/ErrorBoundary/index.native.js b/src/components/ErrorBoundary/index.native.js new file mode 100644 index 000000000000..d320b46984e0 --- /dev/null +++ b/src/components/ErrorBoundary/index.native.js @@ -0,0 +1,16 @@ +import crashlytics from '@react-native-firebase/crashlytics'; + +import BaseErrorBoundary from './BaseErrorBoundary'; +import Log from '../../libs/Log'; + +BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => { + // Log the error to the server + Log.alert(errorMessage, 0, {error: error.message, errorInfo}, false); + + /* On native we also log the error to crashlytics + * Since the error was handled we need to manually tell crashlytics about it */ + crashlytics().log(`errorInfo: ${JSON.stringify(errorInfo)}`); + crashlytics().recordError(error); +}; + +export default BaseErrorBoundary; diff --git a/tests/unit/loginTest.js b/tests/unit/loginTest.js index c8bcc6c6f00f..615790e4b38a 100644 --- a/tests/unit/loginTest.js +++ b/tests/unit/loginTest.js @@ -9,6 +9,13 @@ import React from 'react'; import renderer from 'react-test-renderer'; import App from '../../src/App'; +/* uses and we need to mock the imported crashlytics module +* due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 */ +jest.mock('@react-native-firebase/crashlytics', () => () => ({ + log: jest.fn(), + recordError: jest.fn(), +})); + describe('AppComponent', () => { it('renders correctly', () => { renderer.create();