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** image ### **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",