From 3091e43fee7490b59217e7237a9b4a7e02ef8eb6 Mon Sep 17 00:00:00 2001
From: Frank von Hoven <141057783+frankvonhoven@users.noreply.github.com>
Date: Fri, 8 Nov 2024 12:20:32 -0600
Subject: [PATCH] feat: 1957 crash screen redesign (#12015)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Refactors ErrorBoundary to match [updated designs in
Figma](https://www.figma.com/design/DM5G1pyp74sMJwyKbw1KiR/Error-message-and-bug-report?node-id=1-5473&node-type=section&t=98CWfEC4JrKwpNCE-0)
## **Related issues**
Fixes: [#1957](https://github.com/MetaMask/mobile-planning/issues/1957)
## **Manual testing steps**
1. Go WalletView
2. Add:
```
useEffect(() => {
throw new Error('Test Error');
}, []);
```
3. Restart the app
4. Dismiss the Red Screen
5. See the new Error Screen
## TESTING THE BUILD:
1. Create a QA build in Bitrise
2. force an error
3. report the error
4. check
[Sentry](https://metamask.sentry.io/feedback/?feedbackSlug=test-metamask-mobile%3A6031686409&mailbox=ignored&project=2651591&referrer=feedback_list_page&statsPeriod=30d)
5. Should see user feedback in the Sentry report
6. Example:
![image](https://github.com/user-attachments/assets/c1737758-5739-48f9-96d1-798e523f5be3)
## **Screenshots/Recordings**
### **Before**
### **After**
https://github.com/user-attachments/assets/7dbe5e04-0152-41bc-984a-37cba98c4eeb
## **Pre-merge author checklist**
- [ ] I’ve followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---------
Co-authored-by: Daniel Cross
---
app/components/Views/ErrorBoundary/index.js | 387 +++++++++----
.../Views/ErrorBoundary/warning-icon.png | Bin 0 -> 1662 bytes
.../Login/__snapshots__/index.test.tsx.snap | 533 ++++++++----------
locales/languages/en.json | 14 +-
4 files changed, 529 insertions(+), 405 deletions(-)
create mode 100644 app/components/Views/ErrorBoundary/warning-icon.png
diff --git a/app/components/Views/ErrorBoundary/index.js b/app/components/Views/ErrorBoundary/index.js
index a0730857d36e..f300d534ea32 100644
--- a/app/components/Views/ErrorBoundary/index.js
+++ b/app/components/Views/ErrorBoundary/index.js
@@ -1,12 +1,17 @@
-import React, { Component, useCallback } from 'react';
+import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
View,
StyleSheet,
- Image,
Linking,
Alert,
+ Platform,
+ Modal,
+ KeyboardAvoidingView,
+ DevSettings,
+ Image,
+ TextInput,
} from 'react-native';
import PropTypes from 'prop-types';
import { lastEventId as getLatestSentryId } from '@sentry/react-native';
@@ -16,39 +21,49 @@ import Logger from '../../../util/Logger';
import { fontStyles } from '../../../styles/common';
import { ScrollView } from 'react-native-gesture-handler';
import { strings } from '../../../../locales/i18n';
-import Icon from 'react-native-vector-icons/FontAwesome';
+import CLIcon, {
+ IconColor,
+ IconName,
+ IconSize,
+} from '../../../component-library/components/Icons/Icon';
import ClipboardManager from '../../../core/ClipboardManager';
import { mockTheme, ThemeContext, useTheme } from '../../../util/theme';
import { SafeAreaView } from 'react-native-safe-area-context';
+import BannerAlert from '../../../component-library/components/Banners/Banner/variants/BannerAlert';
+import { BannerAlertSeverity } from '../../../component-library/components/Banners/Banner/variants/BannerAlert/BannerAlert.types';
+import CLText, {
+ TextVariant,
+} from '../../../component-library/components/Texts/Text';
import {
MetaMetricsEvents,
withMetricsAwareness,
} from '../../../components/hooks/useMetrics';
+import AppConstants from '../../../core/AppConstants';
+import { useSelector } from 'react-redux';
// eslint-disable-next-line import/no-commonjs
-const metamaskErrorImage = require('../../../images/metamask-error.png');
+const WarningIcon = require('./warning-icon.png');
const createStyles = (colors) =>
StyleSheet.create({
container: {
flex: 1,
+ paddingHorizontal: 8,
backgroundColor: colors.background.default,
},
- content: {
- paddingHorizontal: 24,
- flex: 1,
- },
header: {
alignItems: 'center',
+ paddingTop: 20,
},
errorImage: {
- width: 50,
- height: 50,
- marginTop: 24,
+ width: 32,
+ height: 32,
},
title: {
color: colors.text.default,
fontSize: 24,
+ paddingTop: 10,
+ paddingBottom: 20,
lineHeight: 34,
...fontStyles.bold,
},
@@ -60,23 +75,36 @@ const createStyles = (colors) =>
textAlign: 'center',
...fontStyles.normal,
},
- errorContainer: {
+ errorMessageContainer: {
+ flexShrink: 1,
backgroundColor: colors.error.muted,
borderRadius: 8,
- marginTop: 24,
+ marginTop: 10,
+ padding: 10,
},
error: {
- color: colors.text.default,
+ color: colors.error.default,
padding: 8,
fontSize: 14,
lineHeight: 20,
...fontStyles.normal,
},
button: {
- marginTop: 24,
+ marginTop: 16,
borderColor: colors.primary.default,
borderWidth: 1,
- borderRadius: 50,
+ borderRadius: 48,
+ height: 48,
+ padding: 12,
+ paddingHorizontal: 34,
+ },
+ blueButton: {
+ marginTop: 16,
+ borderColor: colors.primary.default,
+ backgroundColor: colors.primary.default,
+ borderWidth: 1,
+ borderRadius: 48,
+ height: 48,
padding: 12,
paddingHorizontal: 34,
},
@@ -85,6 +113,55 @@ const createStyles = (colors) =>
textAlign: 'center',
...fontStyles.normal,
},
+ blueButtonText: {
+ color: colors.background.default,
+ textAlign: 'center',
+ ...fontStyles.normal,
+ },
+ submitButton: {
+ width: '45%',
+ backgroundColor: colors.primary.default,
+ marginTop: 24,
+ borderColor: colors.primary.default,
+ borderWidth: 1,
+ borderRadius: 48,
+ height: 48,
+ padding: 12,
+ paddingHorizontal: 34,
+ },
+ cancelButton: {
+ width: '45%',
+ marginTop: 24,
+ borderColor: colors.primary.default,
+ borderWidth: 1,
+ borderRadius: 48,
+ height: 48,
+ padding: 12,
+ paddingHorizontal: 34,
+ },
+ buttonsContainer: {
+ flexGrow: 1,
+ bottom: 10,
+ justifyContent: 'flex-end',
+ },
+ modalButtonsWrapper: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ alignItems: 'flex-end',
+ bottom: 24,
+ paddingHorizontal: 10,
+ },
+ feedbackInput: {
+ borderColor: colors.primary.default,
+ minHeight: 175,
+ minWidth: '100%',
+ paddingHorizontal: 16,
+ paddingTop: 10,
+ borderRadius: 10,
+ borderWidth: 1,
+ marginTop: 20,
+ },
textContainer: {
marginTop: 24,
},
@@ -105,120 +182,216 @@ const createStyles = (colors) =>
reportStep: {
marginTop: 14,
},
+ banner: {
+ width: '100%',
+ marginTop: 20,
+ paddingHorizontal: 16,
+ },
+ keyboardViewContainer: { flex: 1, justifyContent: 'flex-end' },
+ modalWrapper: { flex: 1, justifyContent: 'space-between' },
+ modalTopContainer: { flex: 1, paddingTop: '20%', paddingHorizontal: 16 },
+ closeIconWrapper: {
+ position: 'absolute',
+ right: 0,
+ top: 2,
+ bottom: 0,
+ },
+ modalTitleWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ modalTitleText: { paddingTop: 0 },
+ errorBoxTitle: { fontWeight: '600' },
+ contentContainer: {
+ justifyContent: 'space-between',
+ flex: 1,
+ paddingHorizontal: 16,
+ },
+ errorContentWrapper: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 20,
+ },
+ row: { flexDirection: 'row' },
+ copyText: {
+ color: colors.primary.default,
+ fontSize: 14,
+ paddingLeft: 5,
+ fontWeight: '500',
+ },
+ infoBanner: { marginBottom: 20 },
+ hitSlop: { top: 50, right: 50, bottom: 50, left: 50 },
});
-const UserFeedbackSection = ({ styles, sentryId }) => {
- /**
- * Prompt bug report form
- */
- const promptBugReport = useCallback(() => {
- Alert.prompt(
- strings('error_screen.bug_report_prompt_title'),
- strings('error_screen.bug_report_prompt_description'),
- [
- { text: strings('error_screen.cancel'), style: 'cancel' },
- {
- text: strings('error_screen.send'),
- onPress: (comments = '') => {
- // Send Sentry feedback
- captureSentryFeedback({ sentryId, comments });
- Alert.alert(strings('error_screen.bug_report_thanks'));
- },
- },
- ],
- );
- }, [sentryId]);
-
- return (
-
-
- {' '}
- {strings('error_screen.submit_ticket_8')}{' '}
-
- {strings('error_screen.submit_ticket_6')}
- {' '}
- {strings('error_screen.submit_ticket_9')}
-
+export const Fallback = (props) => {
+ const { colors } = useTheme();
+ const styles = createStyles(colors);
+ const [modalVisible, setModalVisible] = React.useState(false);
+ const [feedback, setFeedback] = React.useState('');
+ const dataCollectionForMarketing = useSelector(
+ (state) => state.security.dataCollectionForMarketing,
);
-};
-UserFeedbackSection.propTypes = {
- styles: PropTypes.object,
- sentryId: PropTypes.string,
-};
+ const toggleModal = () => {
+ setModalVisible((visible) => !visible);
+ setFeedback('');
+ };
+ const handleContactSupport = () =>
+ Linking.openURL(AppConstants.REVIEW_PROMPT.SUPPORT);
+ const handleTryAgain = () => DevSettings.reload();
-const Fallback = (props) => {
- const { colors } = useTheme();
- const styles = createStyles(colors);
+ const handleSubmit = () => {
+ toggleModal();
+ captureSentryFeedback({ sentryId: props.sentryId, comments: feedback });
+ Alert.alert(strings('error_screen.bug_report_thanks'));
+ };
return (
-
+
-
+
{strings('error_screen.title')}
- {strings('error_screen.subtitle')}
-
-
- {props.errorMessage}
-
-
-
-
- {' '}
- {strings('error_screen.try_again_button')}
+ {strings('error_screen.subtitle')}}
+ />
+
+ {strings('error_screen.save_seedphrase_1')}{' '}
+
+ {strings('error_screen.save_seedphrase_2')}
+ {' '}
+ {strings('error_screen.save_seedphrase_3')}
+ }
+ />
+
+
+ {strings('error_screen.error_message')}
+
+
+
+ {strings('error_screen.copy')}
-
-
- {strings('error_screen.submit_ticket_1')}
-
-
-
-
- {' '}
- {strings('error_screen.submit_ticket_2')}
-
-
-
-
- {' '}
-
- {strings('error_screen.submit_ticket_3')}
- {' '}
- {strings('error_screen.submit_ticket_4')}
+
+
+ {props.errorMessage}
+
+
+
+ {dataCollectionForMarketing && (
+
+
+ {strings('error_screen.describe')}
+
+
+ )}
+
+
+ {strings('error_screen.contact_support')}
-
-
-
- {' '}
- {strings('error_screen.submit_ticket_5')}{' '}
-
- {strings('error_screen.submit_ticket_6')}
- {' '}
- {strings('error_screen.submit_ticket_7')}
+
+
+
+ {strings('error_screen.try_again')}
-
-
-
- {strings('error_screen.save_seedphrase_1')}{' '}
-
- {strings('error_screen.save_seedphrase_2')}
- {' '}
- {strings('error_screen.save_seedphrase_3')}
-
+
-
+
+
+
+
+
+
+ {strings('error_screen.modal_title')}
+
+
+
+
+
+
+
+
+
+
+ {strings('error_screen.cancel')}
+
+
+
+
+ {strings('error_screen.submit')}
+
+
+
+
+
+
+
);
};
Fallback.propTypes = {
errorMessage: PropTypes.string,
- resetError: PropTypes.func,
showExportSeedphrase: PropTypes.func,
copyErrorToClipboard: PropTypes.func,
- openTicket: PropTypes.func,
sentryId: PropTypes.string,
};
diff --git a/app/components/Views/ErrorBoundary/warning-icon.png b/app/components/Views/ErrorBoundary/warning-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..91a9fdd9fd927b48d4a29ccb12dfe4daec53100d
GIT binary patch
literal 1662
zcmV-^27&pBP)a_nSR3M;U6?9kkN>z%#X$^7ikII(@7H1e)zcD?iVoA14OGp^A+fiV{Fk5Kw*
z&dan`tBq-s!8;!Ea?Hz&nK9v|pBQO#fDZ9;!HT255ys%1*m1mh7bds|tt&hjKw!3(
zhG2^!;KKm2Ap%+_umz2MV?gTy{;-j)C$rg%b-P^_1OY>Hl%1WO_`Atu;xa^=Rvm0H
z42(S)jYgTys|9svW3wSIby{VxMI4|7-g3QOHyS_`v~e(^ORI{61x7TRO{W1w3B!;%
z5YQ@uErJ^@`u%?PZ9n{o9slq-oBVUgMt^A5=pFF?|6F3Ww_{WK^4jpMU8Z@8J<8ySB4*~1_
z_P%*W!xuO4F<8Jw(#D1kXTu3qKxHN*0%i2wV1Y0IaPiBRF3t3b5ep>?}Qxo<63X$Dfc3;qD`9KKV2~$32F_p{aSDEJHC5Dh-E(1rt-fEQSDv
zfldo_P(_lvOyb_cc1dZGtjYwVFyZZG#IU`a+W-LCwp)$#985SrKet^{nhm9yU~`EW
z#}Gr=CA!2=dmqjMumP-y@3SPAS%nEk%fqdyF0l-FKvmdfmJ!KPOyEBR19WwDcu;78
z0ECc7gxdSy4R?OJN;_*BE~?7}8^8(!dYlbiDuBXF$m0@a*jyqEaQXZdojrRQADd9n
zWgZn}Lz&?K7HEG2;-$Jon;4E?{;xe&0hft;30z)Yx?JWl6+j^)BrJ$b^{rMbJyt{D
zs^F8Yc-RQ2j8lN5PDnXXlnF*ht-8bm2MdY<22fq5LnL#%%-lvuTw>$-GR!EHThkpz|ju
z?mUAOLG0W<}EBGOfuMs}T}66PfB&FNx
zg(G$_22e&LxLl$vhUok<#n1!{2{}oFXR^3VEDmc$Qfq`nNOC7m??MbskbfHnaIis}
ziZe06>Jn8klv81M4b^3W4MkINw~XMLC8`2cMGR#PsiG$cpbE?@+N#UcF~RB*Wh)m=
znW+GkaXmUvWk~5#ahPdZkP-Y$QpLwA(tQD`xUJ8AEwy^3GNJM+(tV*!Doz}vOt7(_
z%qr4-p-d`HW`dOkMOTsX0NJFkzWXzNa9d`n2-H-ZLnPx=T+9S)9pE1qx>jo96fRMo
z&X$F`xB@zZU9!rT1q_Sar2rOfuy|v@o?ybhvBlnA=G@o(VEFG%Y5)MofBgN5e4t<|
z?m!qpyGIbNrphXk4fO}N-}uK5s_T(=Glq5
zY6Z`XXSX1`MVOy-ZLP7*Hi{zazT{OYc*b_SooBOnoFkMcP_PKpB%t>LQ(8=?r>TqN
zCzv6%=>0(p2EaikPDqjr9ol^KoO_#)nr43gy0a=kpCYxtHH#qvTDZ-SHUmSFhJXy`
z#x3@4DGuS|YX9TpHlTBuG+60hX!lxTkUFi0G0^#GBKTIk1u@5c9PXOiIsgCw07*qo
IM6N<$f~jTl^8f$<
literal 0
HcmV?d00001
diff --git a/app/components/Views/Login/__snapshots__/index.test.tsx.snap b/app/components/Views/Login/__snapshots__/index.test.tsx.snap
index df38946a89c3..4beae542b9e8 100644
--- a/app/components/Views/Login/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Login/__snapshots__/index.test.tsx.snap
@@ -6,401 +6,344 @@ exports[`Login should render correctly 1`] = `
{
"backgroundColor": "#ffffff",
"flex": 1,
+ "paddingHorizontal": 8,
}
}
>
-
-
-
+
+
-
+
+
+
+
-
+
- An error occurred
-
+ }
+ >
Your information can't be shown. Don’t worry, your wallet and funds are safe.
+
+
-
- View: Login
-TypeError: (0 , _reactNativeDeviceInfo.getTotalMemorySync) is not a function
-
+ width={24}
+ />
-
+ If you keep getting this error,
+
-
-
-
-
- Try again
+ save your Secret Recovery Phrase
-
+
+ and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet.
+
-
+
+
-
+
+
-
- Please report this issue so we can fix it:
-
-
-
+
+ Copy
+
+
+
+
+
+
-
-
-
-
- Take a screenshot of this screen.
-
-
-
-
-
-
-
- Copy
-
-
- the error message to clipboard.
-
-
-
-
-
-
- Submit a ticket
-
-
- here.
-
-
- Please include the error message and the screenshot.
-
-
-
-
-
-
- Send us a bug report
-
-
- here.
-
-
- Please include details about what happened.
+ View: Login
+TypeError: (0 , _reactNativeDeviceInfo.getTotalMemorySync) is not a function
+
+
+
+
- If this error persists,
-
-
+
+
+
- save your Secret Recovery Phrase
-
-
- & re-install the app. Note: you can NOT restore your wallet without your Secret Recovery Phrase.
+ }
+ >
+ Try again
-
+
-
+
+
`;
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 35616e1495c4..26eba78c3217 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -2930,13 +2930,21 @@
"bug_report_prompt_title": "Tell us what happened",
"bug_report_prompt_description": "Add details so we can figure out what went wrong.",
"bug_report_thanks": "Thanks! We’ll take a look soon.",
- "save_seedphrase_1": "If this error persists,",
+ "save_seedphrase_1": "If you keep getting this error,",
"save_seedphrase_2": "save your Secret Recovery Phrase",
- "save_seedphrase_3": "& re-install the app. Note: you can NOT restore your wallet without your Secret Recovery Phrase.",
+ "save_seedphrase_3": "and re-install the app. Remember: without your Secret Recovery Phrase, you can't restore your wallet.",
"copied_clipboard": "Copied to clipboard",
"ok": "OK",
"cancel": "Cancel",
- "send": "Send"
+ "send": "Send",
+ "submit": "Submit",
+ "modal_title": "Describe what happened",
+ "modal_placeholder": "Sharing details like how we can reproduce the bug will help us fix the problem.",
+ "error_message": "Error message:",
+ "copy": "Copy",
+ "describe": "Describe what happened",
+ "try_again": "Try again",
+ "contact_support": "Contact support"
},
"whats_new": {
"title": "What's new",