From f0acd52c6fcd934c1e896be6f53f175d2be26b52 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 4 Oct 2024 00:53:18 +0100 Subject: [PATCH] feat(1852): Implement sentry user report on error screen --- app/_locales/de/messages.json | 12 - app/_locales/el/messages.json | 12 - app/_locales/en/messages.json | 64 +++++- app/_locales/en_GB/messages.json | 12 - app/_locales/es/messages.json | 12 - app/_locales/es_419/messages.json | 12 - app/_locales/fr/messages.json | 12 - app/_locales/hi/messages.json | 12 - app/_locales/id/messages.json | 12 - app/_locales/it/messages.json | 4 - app/_locales/ja/messages.json | 12 - app/_locales/ko/messages.json | 12 - app/_locales/ph/messages.json | 12 - app/_locales/pt/messages.json | 12 - app/_locales/pt_BR/messages.json | 12 - app/_locales/ru/messages.json | 12 - app/_locales/tl/messages.json | 12 - app/_locales/tr/messages.json | 12 - app/_locales/vi/messages.json | 12 - app/_locales/zh_CN/messages.json | 12 - app/_locales/zh_TW/messages.json | 12 - privacy-snapshot.json | 2 + shared/constants/metametrics.ts | 4 + shared/modules/i18n.test.ts | 16 +- shared/modules/i18n.ts | 3 +- test/data/mock-state.json | 8 +- test/e2e/page-objects/pages/ErrorPage.ts | 78 +++++++ .../page-objects/pages/deveoper-options.ts | 38 +++ .../pages/experimental-settings.ts | 2 +- test/e2e/page-objects/pages/settings-page.ts | 10 + .../account/snap-account-settings.spec.ts | 2 +- .../metrics/developer-options-sentry.spec.ts | 60 +++++ ui/ducks/locale/locale.js | 42 ---- ui/ducks/locale/locale.test.ts | 80 +++++-- ui/ducks/locale/locale.ts | 108 +++++++++ ui/pages/error/error-component.test.tsx | 132 +++++++++++ ui/pages/error/error.component.js | 104 --------- ui/pages/error/error.component.tsx | 216 ++++++++++++++++++ ui/pages/error/index.scss | 58 +++-- .../developer-options-tab.test.tsx.snap | 46 +++- .../developer-options-tab.tsx | 2 +- .../developer-options-tab/sentry-test.tsx | 66 +++++- ui/pages/settings/settings.component.js | 5 +- 43 files changed, 923 insertions(+), 455 deletions(-) create mode 100644 test/e2e/page-objects/pages/ErrorPage.ts create mode 100644 test/e2e/page-objects/pages/deveoper-options.ts create mode 100644 test/e2e/tests/metrics/developer-options-sentry.spec.ts delete mode 100644 ui/ducks/locale/locale.js create mode 100644 ui/ducks/locale/locale.ts create mode 100644 ui/pages/error/error-component.test.tsx delete mode 100644 ui/pages/error/error.component.js create mode 100644 ui/pages/error/error.component.tsx diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 9931e17a83a7..63c9bdabf934 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1720,10 +1720,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Fehlerdetails", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Fehler beim Abrufen der Liste sicherer Ketten, bitte mit Vorsicht fortfahren." }, @@ -1735,14 +1731,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Versuchen Sie es erneut, indem Sie die Seite neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Versuchen Sie es erneut, indem das Popup schließen und neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask hat einen Fehler festgestellt.", "description": "Title of generic error page" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 95e1e43cf51f..e5de7913bd4b 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1720,10 +1720,6 @@ "message": "Κωδικός: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Λεπτομέρειες σφάλματος", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Σφάλμα κατά τη λήψη της λίστας ασφαλών αλυσίδων, συνεχίστε με προσοχή." }, @@ -1735,14 +1731,6 @@ "message": "Κωδικός: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Προσπαθήστε ξανά με επαναφόρτωση της σελίδας ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Προσπαθήστε ξανά κλείνοντας και ανοίγοντας ξανά το αναδυόμενο παράθυρο ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Το MetaMask αντιμετώπισε ένα σφάλμα", "description": "Title of generic error page" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 862b761abd8f..8da3dd3fee0c 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1920,10 +1920,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1935,18 +1931,66 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageContactSupport": { + "message": "Contact support", + "description": "Button for contact MM support" + }, + "errorPageDescribeUsWhatHappened": { + "message": "Describe what happened", + "description": "Button for submitting report to sentry" + }, + "errorPageInfo": { + "message": "Your information can’t be shown. Don’t worry, your wallet and funds are safe.", + "description": "Information banner shown in the error page" + }, + "errorPageMessageTitle": { + "message": "Error message", + "description": "Title for description, which is displayed for debugging purposes" + }, + "errorPageSentryCancelButtonLabel": { + "message": "Cancel", + "description": "In sentry feedback form, The label of cancel buttons used in the feedback form." + }, + "errorPageSentryConfirmButtonLabel": { + "message": "Confirm", + "description": "In sentry feedback form, The label of confirm buttons used in the feedback form." + }, + "errorPageSentryFormTitle": { + "message": "Describe what happened", + "description": "In sentry feedback form, The title at the top of the feedback form." }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageSentryIsRequiredLabel": { + "message": "(required)", + "description": "In sentry feedback form, The label shown next to an input field that is required." + }, + "errorPageSentryMessageLabel": { + "message": "Description", + "description": "In sentry feedback form, The label for the feedback description input field." + }, + "errorPageSentryMessagePlaceholder": { + "message": "Sharing details like the error message you saw can help us fix the problem.", + "description": "In sentry feedback form, The placeholder for the feedback description input field." + }, + "errorPageSentrySubmitButtonLabel": { + "message": "Submit", + "description": "In sentry feedback form, The label of the submit button used in the feedback form." + }, + "errorPageSentrySuccessMessageText": { + "message": "Thanks! We will take a look soon.", + "description": "In sentry feedback form, The message displayed after a successful feedback submission." + }, + "errorPageSentryTriggerLabel": { + "message": "Describe what happened", + "description": "In sentry feedback form, The label of the injected button that opens up the feedback form when clicked." }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" }, + "errorPageTryAgain": { + "message": "Try again", + "description": "Button for try again" + }, "errorStack": { "message": "Stack:", "description": "Title for error stack, which is displayed for debugging purposes" diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 25cb6cd3df29..e03359463b1e 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1795,10 +1795,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1810,14 +1806,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 9fd0f3d20941..40ca141afa50 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1717,10 +1717,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error al obtener la lista de cadenas seguras, por favor continúe con precaución." }, @@ -1732,14 +1728,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index cd980aaa99c2..c77406b85f1b 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -703,10 +703,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensaje: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -715,14 +711,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 05c67e49462f..d8cd1679ae33 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1720,10 +1720,6 @@ "message": "Code : $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Détails de l’erreur", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erreur lors de l’obtention de la liste des chaînes sécurisées, veuillez continuer avec précaution." }, @@ -1735,14 +1731,6 @@ "message": "Code : $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Essayez à nouveau en rechargeant la page, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Réessayez en fermant puis en rouvrant le pop-up, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask a rencontré une erreur", "description": "Title of generic error page" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index e23b10a874f0..308d8835d628 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1720,10 +1720,6 @@ "message": "कोड: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "गड़बड़ी की जानकारी", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "सेफ चेन लिस्ट पाते समय गड़बड़ी हुई, कृपया सावधानी के साथ जारी रखें।" }, @@ -1735,14 +1731,6 @@ "message": "कोड: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "पेज को दोबारा लोड करके फिर से कोशिश करें या सपोर्ट $1 से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "पॉपअप को बंद करके और फिर से खोलने की कोशिश करें या $1 पर सपोर्ट से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask में कोई गड़बड़ी हुई", "description": "Title of generic error page" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 12ab926cf9ce..4b46343ce7f2 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1720,10 +1720,6 @@ "message": "Kode: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detail kesalahan", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Terjadi kesalahan saat mendapatkan daftar rantai aman, lanjutkan dengan hati-hati." }, @@ -1735,14 +1731,6 @@ "message": "Kode: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Coba lagi dengan memuat kembali halaman, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Coba lagi dengan menutup dan membuka kembali sembulan, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask mengalami kesalahan", "description": "Title of generic error page" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 70e81c595852..456d5ab1d3fe 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -753,10 +753,6 @@ "message": "Codice: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Dettagli Errore", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Messaggio: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 0c3887643691..ad9f6ec593e3 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1720,10 +1720,6 @@ "message": "コード: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "エラーの詳細", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "安全なチェーンリストの取得中にエラーが発生しました。慎重に続けてください。" }, @@ -1735,14 +1731,6 @@ "message": "コード: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "ページを再ロードしてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "ポップアップを閉じてから再び開いてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMaskにエラーが発生しました", "description": "Title of generic error page" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6e4dad181512..405382c15865 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1720,10 +1720,6 @@ "message": "코드: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "오류 세부 정보", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "안전 체인 목록을 가져오는 동안 오류가 발생했습니다. 주의하여 계속 진행하세요." }, @@ -1735,14 +1731,6 @@ "message": "코드: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "페이지를 새로고침하여 다시 시도하거나 $1로 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "팝업을 닫은 후 다시 열어 다시 시도하거나 $1에서 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 오류 발생", "description": "Title of generic error page" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index e12eb4379cf1..988df4005efc 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -440,10 +440,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensahe: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -452,14 +448,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukan ulit sa pamamagitan ng pag-reload ng page, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan ulit sa pamamagitan ng pagsara at pagbukas ulit ng popup, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 47a53a6ed328..a1cf53ce358f 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1720,10 +1720,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erro ao obter uma lista segura da cadeia. Por favor, prossiga com cautela." }, @@ -1735,14 +1731,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 2becf1c495a1..336f9af56c31 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -703,10 +703,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensagem: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -715,14 +711,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index 0ecd4f0eb8d6..69454a8fb86e 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1720,10 +1720,6 @@ "message": "Код: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Сведения об ошибке", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Ошибка при получении списка безопасных блокчейнов. Продолжайте с осторожностью." }, @@ -1735,14 +1731,6 @@ "message": "Код: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Повторите попытку, перезагрузив страницу, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Повторите попытку, закрыв и вновь открыв всплывающее окно, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask обнаружил ошибку", "description": "Title of generic error page" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index c6614483aa5b..31d38cab9620 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1720,10 +1720,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "May error habang kinukuha ang ligtas na chain list, mangyaring magpatuloy nang may pag-iingat." }, @@ -1735,14 +1731,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukang muling i-reload ang page, o kontakin ang support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan muli sa pamamagitan ng pagsasara o muling pagbubukas ng pop-up, kontakin ang support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 361b92cdd87e..daadbfd09a1c 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1720,10 +1720,6 @@ "message": "Kod: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Hata ayrıntıları", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Güvenli zincir listesi alınırken hata oluştu, lütfen dikkatli bir şekilde devam edin." }, @@ -1735,14 +1731,6 @@ "message": "Kod: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Sayfayı yeniden yükleyerek tekrar deneyin veya $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Açılır pencereyi kapatarak ve yeniden açarak tekrar deneyin $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask bir hata ile karşılaştı", "description": "Title of generic error page" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 3be725af9351..b870fcfa427e 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1720,10 +1720,6 @@ "message": "Mã: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Chi tiết về lỗi", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Lỗi khi lấy danh sách chuỗi an toàn, vui lòng tiếp tục một cách thận trọng." }, @@ -1735,14 +1731,6 @@ "message": "Mã: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Hãy thử lại bằng cách tải lại trang hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Hãy thử lại bằng cách đóng và mở lại cửa sổ bật lên hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask đã gặp lỗi", "description": "Title of generic error page" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 58298abdf542..1dd698f38d3b 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1720,10 +1720,6 @@ "message": "代码:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "错误详情", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "获取安全链列表时出错,请谨慎继续。" }, @@ -1735,14 +1731,6 @@ "message": "代码:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "通过重新加载页面再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "通过关闭并重新打开弹出窗口再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遇到了一个错误", "description": "Title of generic error page" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 32e98ed12288..e2e86e78cc0f 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -442,10 +442,6 @@ "message": "代碼:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "錯誤詳細資訊", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "訊息:$1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -454,14 +450,6 @@ "message": "代碼:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "重新整理頁面然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "重新開啟彈跳視窗然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遭遇錯誤", "description": "Title of generic error page" diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 2516654f1803..4ab7b30c89e0 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -52,6 +52,8 @@ "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", + "support.metamask.io", + "support.metamask-institutional.io", "swap.api.cx.metamask.io", "test.metamask-phishing.io", "token.api.cx.metamask.io", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 544d24ce1271..8dc84c4c7c92 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -146,6 +146,10 @@ export type MetaMetricsEventOptions = { * as not conforming to our schema. */ matomoEvent?: boolean; + /** + * Values that can used in the "properties" tracking object as keys, + */ + contextPropsIntoEventProperties?: string | string[]; }; export type MetaMetricsEventFragment = { diff --git a/shared/modules/i18n.test.ts b/shared/modules/i18n.test.ts index 8452ef48238c..7a2f49bd9f68 100644 --- a/shared/modules/i18n.test.ts +++ b/shared/modules/i18n.test.ts @@ -109,7 +109,21 @@ describe('I18N Module', () => { ); }); - it('throws if test env set', () => { + it('throws if IN_TEST is set true', () => { + expect(() => + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toThrow( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ); + }); + + it('throws if ENABLE_SETTINGS_PAGE_DEV_OPTIONS is set true', () => { + process.env.IN_TEST = String(false); + process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS = String(true); expect(() => getMessage( FALLBACK_LOCALE, diff --git a/shared/modules/i18n.ts b/shared/modules/i18n.ts index d19cfa4b3b11..b5c22c869b54 100644 --- a/shared/modules/i18n.ts +++ b/shared/modules/i18n.ts @@ -177,7 +177,7 @@ function missingKeyError( onError?.(error); log.error(error); - if (process.env.IN_TEST) { + if (process.env.IN_TEST || process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) { throw error; } } @@ -188,7 +188,6 @@ function missingKeyError( warned[localeCode] = warned[localeCode] ?? {}; warned[localeCode][key] = true; - log.warn( `Translator - Unable to find value of key "${key}" for locale "${localeCode}"`, ); diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 654e915a1305..ef36b4b09d2a 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -46,7 +46,13 @@ "mostRecentOverviewPage": "/mostRecentOverviewPage" }, "localeMessages": { - "currentLocale": "en" + "currentLocale": "en", + "current": { + "user": "user" + }, + "en": { + "user": "user" + } }, "metamask": { "use4ByteResolution": true, diff --git a/test/e2e/page-objects/pages/ErrorPage.ts b/test/e2e/page-objects/pages/ErrorPage.ts new file mode 100644 index 000000000000..313e9ec806de --- /dev/null +++ b/test/e2e/page-objects/pages/ErrorPage.ts @@ -0,0 +1,78 @@ +import { strict as assert } from 'assert'; +import { Driver } from '../../webdriver/driver'; +import HeaderNavbar from './header-navbar'; +import SettingsPage from './settings-page'; +import DevelopOptions from './deveoper-options'; + +class ErrorPage { + private readonly driver: Driver; + + // Locators + private readonly errorPageTitle: object = { + text: 'MetaMask encountered an error', + css: 'h3', + }; + + private readonly errorMessage = '[data-testid="error-page-error-message"]'; + + private readonly sendReportToSentryButton = + '[data-testid="error-page-describe-what-happened-button"]'; + + private readonly sentryReportForm = '#sentry-feedback'; + + private readonly contactSupportButton = + '[data-testid="error-page-contact-support-button"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.errorPageTitle); + } catch (e) { + console.log('Timeout while waiting for Error page to be loaded', e); + throw e; + } + console.log('Error page is loaded'); + } + + async triggerPageCrash(): Promise { + const headerNavbar = new HeaderNavbar(this.driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(this.driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptions(this.driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); + } + + async validate_errorMessage(): Promise { + const errorMessageTextDOM = await this.driver.waitForSelector( + this.errorMessage, + ); + const errorMessageText = await errorMessageTextDOM.getText(); + assert.equal( + errorMessageText, + `Message: Unable to find value of key "developerOptions" for locale "en"`, + `Error in loading error page`, + ); + } + + async submitToSentryUserFeedbackForm(): Promise { + console.log(`Open sentry user feedback form in error page`); + await this.driver.clickElement(this.sendReportToSentryButton); + await this.driver.waitForSelector(this.sentryReportForm); + } + + async contactAndValidateMetamaskSupport(): Promise { + console.log(`Contact metamask support form in a separate page`); + await this.driver.clickElement(this.contactSupportButton); + // metamask, help page + await this.driver.waitUntilXWindowHandles(2); + } +} + +export default ErrorPage; diff --git a/test/e2e/page-objects/pages/deveoper-options.ts b/test/e2e/page-objects/pages/deveoper-options.ts new file mode 100644 index 000000000000..c15f6c767a82 --- /dev/null +++ b/test/e2e/page-objects/pages/deveoper-options.ts @@ -0,0 +1,38 @@ +import { Driver } from '../../webdriver/driver'; + +class DevelopOptions { + private readonly driver: Driver; + + // Locators + private readonly generatePageCrashButton: string = + '[data-testid="developer-options-generate-page-crash-button"]'; + + private readonly developOptionsPageTitle: object = { + text: 'Developer Options', + css: 'h4', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.developOptionsPageTitle); + } catch (e) { + console.log( + 'Timeout while waiting for Developer options page to be loaded', + e, + ); + throw e; + } + console.log('Developer option page is loaded'); + } + + async clickGenerateCrashButton(): Promise { + console.log('Generate a page crash in Developer option page'); + await this.driver.clickElement(this.generatePageCrashButton); + } +} + +export default DevelopOptions; diff --git a/test/e2e/page-objects/pages/experimental-settings.ts b/test/e2e/page-objects/pages/experimental-settings.ts index 8c7129b17555..ce0f24162ef2 100644 --- a/test/e2e/page-objects/pages/experimental-settings.ts +++ b/test/e2e/page-objects/pages/experimental-settings.ts @@ -9,7 +9,7 @@ class ExperimentalSettings { private readonly experimentalPageTitle: object = { text: 'Experimental', - css: '.h4', + css: 'h4', }; constructor(driver: Driver) { diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 547f9e43a34e..c029e34efc7e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -9,6 +9,11 @@ class SettingsPage { css: '.tab-bar__tab__content__title', }; + private readonly developerOptionsButton: object = { + text: 'Developer Options', + css: '.tab-bar__tab__content__title', + }; + private readonly settingsPageTitle: object = { text: 'Settings', css: 'h3', @@ -32,6 +37,11 @@ class SettingsPage { console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } + + async goToDevelopOptionSettings(): Promise { + console.log('Navigating to Develop options page'); + await this.driver.clickElement(this.developerOptionsButton); + } } export default SettingsPage; diff --git a/test/e2e/tests/account/snap-account-settings.spec.ts b/test/e2e/tests/account/snap-account-settings.spec.ts index cbd5f8814b7b..1a0c761fb4df 100644 --- a/test/e2e/tests/account/snap-account-settings.spec.ts +++ b/test/e2e/tests/account/snap-account-settings.spec.ts @@ -33,7 +33,7 @@ describe('Add snap account experimental settings @no-mmi', function (this: Suite await settingsPage.goToExperimentalSettings(); const experimentalSettings = new ExperimentalSettings(driver); - await settingsPage.check_pageIsLoaded(); + await experimentalSettings.check_pageIsLoaded(); await experimentalSettings.toggleAddAccountSnap(); // Make sure the "Add account Snap" button is visible. diff --git a/test/e2e/tests/metrics/developer-options-sentry.spec.ts b/test/e2e/tests/metrics/developer-options-sentry.spec.ts new file mode 100644 index 000000000000..8020e69ab962 --- /dev/null +++ b/test/e2e/tests/metrics/developer-options-sentry.spec.ts @@ -0,0 +1,60 @@ +import { Suite } from 'mocha'; +import { withFixtures } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import DevelopOptions from '../../page-objects/pages/deveoper-options'; +import ErrorPage from '../../page-objects/pages/ErrorPage'; + +const triggerCrash = async (driver: Driver): Promise => { + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptions(driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); +}; + +describe('Developer Options - Sentry', function (this: Suite) { + it('gives option to cause a page crash and provides sentry form to report', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + ignoredConsoleErrors: ['ignore-all'], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + await errorPage.validate_errorMessage(); + await errorPage.submitToSentryUserFeedbackForm(); + }, + ); + }); + + it('gives option to cause a page crash and offer contact support option', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + ignoredConsoleErrors: ['ignore-all'], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + + await errorPage.contactAndValidateMetamaskSupport(); + }, + ); + }); +}); diff --git a/ui/ducks/locale/locale.js b/ui/ducks/locale/locale.js deleted file mode 100644 index 5118a749ab58..000000000000 --- a/ui/ducks/locale/locale.js +++ /dev/null @@ -1,42 +0,0 @@ -import { createSelector } from 'reselect'; -import * as actionConstants from '../../store/actionConstants'; - -export default function reduceLocaleMessages(state = {}, { type, payload }) { - switch (type) { - case actionConstants.SET_CURRENT_LOCALE: - return { - ...state, - current: payload.messages, - currentLocale: payload.locale, - }; - default: - return state; - } -} - -/** - * This selector returns a code from file://./../../../app/_locales/index.json. - * - * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. - * - * @param state - * @returns {string} the user's selected locale. - * These codes are not safe to use with the Intl API. - */ -export const getCurrentLocale = (state) => state.localeMessages.currentLocale; - -/** - * This selector returns a - * [BCP 47 Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) - * for use with the Intl API. - * - * @returns {Intl.UnicodeBCP47LocaleIdentifier} the user's selected locale. - */ -export const getIntlLocale = createSelector( - getCurrentLocale, - (locale) => Intl.getCanonicalLocales(locale?.replace(/_/gu, '-'))[0], -); - -export const getCurrentLocaleMessages = (state) => state.localeMessages.current; - -export const getEnLocaleMessages = (state) => state.localeMessages.en; diff --git a/ui/ducks/locale/locale.test.ts b/ui/ducks/locale/locale.test.ts index 37fc1f99e29a..67627bb73423 100644 --- a/ui/ducks/locale/locale.test.ts +++ b/ui/ducks/locale/locale.test.ts @@ -1,32 +1,80 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import locales from '../../../app/_locales/index.json'; -import { getIntlLocale } from './locale'; +import testData from '../../../test/data/mock-state.json'; +import { + getCurrentLocale, + getIntlLocale, + getCurrentLocaleMessages, + getEnLocaleMessages, +} from './locale'; + +// Mock state creation functions const createMockStateWithLocale = (locale: string) => ({ localeMessages: { currentLocale: locale }, }); -describe('getIntlLocale', () => { - it('returns the canonical BCP 47 language tag for the currently selected locale', () => { - const mockState = createMockStateWithLocale('ab-cd'); +describe('Locale Selectors', () => { + describe('getCurrentLocale', () => { + it('returns the current locale from the state', () => { + expect(getCurrentLocale(testData)).toBe('en'); + }); - expect(getIntlLocale(mockState)).toBe('ab-CD'); + it('returns undefined if no current locale is set', () => { + const newAppState = { + ...testData, + localeMessages: { + currentLocale: undefined, + }, + }; + expect(getCurrentLocale(newAppState)).toBeUndefined(); + }); }); - it('throws an error if locale cannot be made into BCP 47 language tag', () => { - const mockState = createMockStateWithLocale('xxxinvalid-locale'); + describe('getIntlLocale', () => { + it('returns the canonical BCP 47 language tag for the currently selected locale', () => { + const mockState = createMockStateWithLocale('en_US'); + expect(getIntlLocale(mockState)).toBe('en-US'); + }); - expect(() => getIntlLocale(mockState)).toThrow(); + locales.forEach((locale: { code: string; name: string }) => { + it(`handles supported locale - "${locale.code}"`, () => { + const mockState = createMockStateWithLocale(locale.code); + expect(() => getIntlLocale(mockState)).not.toThrow(); + }); + }); }); - // @ts-expect-error This is missing from the Mocha type definitions - it.each(locales)( - 'handles all supported locales – "%s"', - (locale: { code: string; name: string }) => { - const mockState = createMockStateWithLocale(locale.code); + describe('getCurrentLocaleMessages', () => { + it('returns the current locale messages from the state', () => { + expect(getCurrentLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no current locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + current: undefined, + }, + }; + expect(getCurrentLocaleMessages(newAppState)).toEqual(undefined); + }); + }); - expect(() => getIntlLocale(mockState)).not.toThrow(); - }, - ); + describe('getEnLocaleMessages', () => { + it('returns the English locale messages from the state', () => { + expect(getEnLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no English locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + en: undefined, + }, + }; + expect(getEnLocaleMessages(newAppState)).toBeUndefined(); + }); + }); }); diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts new file mode 100644 index 000000000000..2ca0cbaac4dc --- /dev/null +++ b/ui/ducks/locale/locale.ts @@ -0,0 +1,108 @@ +import { createSelector } from 'reselect'; +import { Action } from 'redux'; // Import types for actions +import * as actionConstants from '../../store/actionConstants'; +import { FALLBACK_LOCALE } from '../../../shared/modules/i18n'; + +/** + * Type for the locale messages part of the state + */ +type LocaleMessagesState = { + current?: { [key: string]: string }; // Messages for the current locale + currentLocale?: string; // User's selected locale (unsafe for Intl API) + en?: { [key: string]: string }; // English locale messages +}; + +/** + * Payload for the SET_CURRENT_LOCALE action + */ +type SetCurrentLocaleAction = Action & { + type: typeof actionConstants.SET_CURRENT_LOCALE; + payload: { + messages: { [key: string]: string }; + locale: string; + }; +}; + +/** + * Type for actions that can be handled by reduceLocaleMessages + */ +type LocaleMessagesActions = SetCurrentLocaleAction; + +/** + * Initial state for localeMessages reducer + */ +const initialState: LocaleMessagesState = {}; + +/** + * Reducer for localeMessages + * + * @param state - The current state + * @param action - The action being dispatched + * @returns The updated locale messages state + */ +export default function reduceLocaleMessages( + // eslint-disable-next-line @typescript-eslint/default-param-last + state: LocaleMessagesState = initialState, + action: LocaleMessagesActions, +): LocaleMessagesState { + switch (action.type) { + case actionConstants.SET_CURRENT_LOCALE: + return { + ...state, + current: action.payload.messages, + currentLocale: action.payload.locale, + }; + default: + return state; + } +} + +/** + * Type for the overall Redux state + */ +type AppState = { + localeMessages: LocaleMessagesState; +}; + +/** + * This selector returns a code from file://./../../../app/_locales/index.json. + * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. + * + * @param state - The overall state + * @returns The user's selected locale + */ +export const getCurrentLocale = (state: AppState): string | undefined => + state.localeMessages.currentLocale; + +/** + * This selector returns a BCP 47 Language Tag for use with the Intl API. + * + * @returns The user's selected locale in BCP 47 format + */ +export const getIntlLocale = createSelector( + getCurrentLocale, + (locale): string => + Intl.getCanonicalLocales( + locale ? locale.replace(/_/gu, '-') : FALLBACK_LOCALE, + )[0], +); + +/** + * This selector returns the current locale messages. + * + * @param state - The overall state + * @returns The current locale's messages + */ +export const getCurrentLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.current; + +/** + * This selector returns the English locale messages. + * + * @param state - The overall state + * @returns The English locale's messages + */ +export const getEnLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.en; diff --git a/ui/pages/error/error-component.test.tsx b/ui/pages/error/error-component.test.tsx new file mode 100644 index 000000000000..3c257da60ef7 --- /dev/null +++ b/ui/pages/error/error-component.test.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import browser from 'webextension-polyfill'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../test/lib/render-helpers'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../contexts/metametrics'; + +import { getMessage } from '../../helpers/utils/i18n-helper'; +// eslint-disable-next-line import/no-restricted-paths +import messages from '../../../app/_locales/en/messages.json'; +import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; +import { + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; +import ErrorPage from './error.component'; + +jest.mock('../../hooks/useI18nContext', () => ({ + useI18nContext: jest.fn(), +})); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + reload: jest.fn(), + }, +})); + +describe('ErrorPage', () => { + const mockTrackEvent = jest.fn(); + const MockError = new Error( + "Cannot read properties of undefined (reading 'message')", + ) as Error & { code?: string }; + MockError.code = '500'; + + const mockI18nContext = jest + .fn() + .mockReturnValue((key: string, variables: string[]) => + getMessage('en', messages, key, variables), + ); + + beforeEach(() => { + jest.clearAllMocks(); + (useI18nContext as jest.Mock).mockImplementation(mockI18nContext); + }); + + it('should render the error message, code, and name if provided', () => { + const { getByTestId } = renderWithProvider( + + + , + ); + + expect( + getByTestId('error-page-error-message').textContent, + ).toMatchInlineSnapshot( + `"Message: Cannot read properties of undefined (reading 'message')"`, + ); + expect( + getByTestId('error-page-error-code').textContent, + ).toMatchInlineSnapshot(`"Code: 500"`); + expect( + getByTestId('error-page-error-name').textContent, + ).toMatchInlineSnapshot(`"Code: Error"`); + }); + + it('should not render error details if no error information is provided', () => { + const error = {}; + + const { queryByTestId } = renderWithProvider( + + + , + ); + + expect(queryByTestId('error-page-error-message')).toBeNull(); + expect(queryByTestId('error-page-error-code')).toBeNull(); + expect(queryByTestId('error-page-error-name')).toBeNull(); + expect(queryByTestId('error-page-error-stack')).toBeNull(); + }); + + it('should render sentry user feedback form', () => { + const { container } = renderWithProvider( + + + , + ); + expect(container.querySelector('#sentry-feedback')).toBeDefined(); + }); + + it('should reload the extension when the "Try Again" button is clicked', () => { + const { getByTestId } = renderWithProvider( + + + , + ); + const tryAgainButton = getByTestId('error-page-try-again-button'); + fireEvent.click(tryAgainButton); + expect(browser.runtime.reload).toHaveBeenCalled(); + }); + + it('should open the support link and track the MetaMetrics event when the "Contact Support" button is clicked', () => { + window.open = jest.fn(); + + const { getByTestId } = renderWithProvider( + + + , + ); + + const contactSupportButton = getByTestId( + 'error-page-contact-support-button', + ); + fireEvent.click(contactSupportButton); + + expect(window.open).toHaveBeenCalledWith(SUPPORT_REQUEST_LINK, '_blank'); + + expect(mockTrackEvent).toHaveBeenCalledWith( + { + category: MetaMetricsEventCategory.Error, + event: MetaMetricsEventName.SupportLinkClicked, + properties: { + url: SUPPORT_REQUEST_LINK, + }, + }, + { + contextPropsIntoEventProperties: [MetaMetricsContextProp.PageTitle], + }, + ); + }); +}); diff --git a/ui/pages/error/error.component.js b/ui/pages/error/error.component.js deleted file mode 100644 index 57a8e40c6473..000000000000 --- a/ui/pages/error/error.component.js +++ /dev/null @@ -1,104 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { getEnvironmentType } from '../../../app/scripts/lib/util'; -import { ENVIRONMENT_TYPE_POPUP } from '../../../shared/constants/app'; -import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; -import { - MetaMetricsContextProp, - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../shared/constants/metametrics'; - -class ErrorPage extends PureComponent { - static contextTypes = { - t: PropTypes.func.isRequired, - trackEvent: PropTypes.func, - }; - - static propTypes = { - error: PropTypes.object.isRequired, - }; - - renderErrorDetail(content) { - return ( -
  • -

    {content}

    -
  • - ); - } - - renderErrorStack(title, stack) { - return ( -
  • - {title} -
    {stack}
    -
  • - ); - } - - render() { - const { error } = this.props; - const { t } = this.context; - - const isPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; - const supportLink = ( - { - this.context.trackEvent( - { - category: MetaMetricsEventCategory.Error, - event: MetaMetricsEventName.SupportLinkClicked, - properties: { - url: SUPPORT_REQUEST_LINK, - }, - }, - { - contextPropsIntoEventProperties: [ - MetaMetricsContextProp.PageTitle, - ], - }, - ); - }} - > - {this.context.t('here')} - - ); - const message = isPopup - ? t('errorPagePopupMessage', [supportLink]) - : t('errorPageMessage', [supportLink]); - - return ( -
    -

    {t('errorPageTitle')}

    -

    {message}

    -
    -
    - {t('errorDetails')} -
      - {error.message - ? this.renderErrorDetail(t('errorMessage', [error.message])) - : null} - {error.code - ? this.renderErrorDetail(t('errorCode', [error.code])) - : null} - {error.name - ? this.renderErrorDetail(t('errorName', [error.name])) - : null} - {error.stack - ? this.renderErrorStack(t('errorStack'), error.stack) - : null} -
    -
    -
    -
    - ); - } -} - -export default ErrorPage; diff --git a/ui/pages/error/error.component.tsx b/ui/pages/error/error.component.tsx new file mode 100644 index 000000000000..5e2c13ff61f3 --- /dev/null +++ b/ui/pages/error/error.component.tsx @@ -0,0 +1,216 @@ +import React, { useEffect, useContext, useRef } from 'react'; +import * as Sentry from '@sentry/browser'; +import browser from 'webextension-polyfill'; +import { + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; + +import { MetaMetricsContext } from '../../contexts/metametrics'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { + BannerAlert, + Box, + Icon, + IconName, + IconSize, + Text, + Button, + ButtonVariant, +} from '../../components/component-library'; +import { + AlignItems, + BackgroundColor, + BlockSize, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextVariant, +} from '../../helpers/constants/design-system'; + +import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; + +type ErrorPageProps = { + error: { + message?: string; + code?: string; + name?: string; + stack?: string; + }; +}; + +const ErrorPage: React.FC = ({ error }) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const sentryButtonRef = useRef(null); + + useEffect(() => { + // Initialize the Sentry feedback integration widget + const feedback = Sentry.feedbackIntegration({ + enableScreenshot: false, + autoInject: false, + showBranding: false, + showName: false, + showEmail: false, + triggerLabel: t('errorPageSentryTriggerLabel'), + formTitle: t('errorPageSentryFormTitle'), + submitButtonLabel: t('errorPageSentrySubmitButtonLabel'), + cancelButtonLabel: t('errorPageSentryCancelButtonLabel'), + confirmButtonLabel: t('errorPageSentryConfirmButtonLabel'), + isRequiredLabel: t('errorPageSentryIsRequiredLabel'), + messageLabel: t('errorPageSentryMessageLabel'), + messagePlaceholder: t('errorPageSentryMessagePlaceholder'), + errorPageSuccessMessageText: t('errorPageSentrySuccessMessageText'), + }); + + let isMounted = true; // For preventing memory leak + + if (sentryButtonRef.current && isMounted) { + feedback.attachTo(sentryButtonRef.current); // Attach feedback widget to button + } + + return () => { + isMounted = false; // Prevents async operations on unmounted component + if (feedback) { + feedback.remove(); // Clean up feedback widget + } + }; + }, [sentryButtonRef, t]); + + return ( +
    +
    + + + + {t('errorPageTitle')} + + + +
    + {t('errorPageInfo')} +
    + + {t('errorPageMessageTitle')} + + + {error.message ? ( + + {t('errorMessage', [error.message])} + + ) : null} + {error.code ? ( + + {t('errorCode', [error.code])} + + ) : null} + {error.name ? ( + + {t('errorName', [error.name])} + + ) : null} + {error.stack ? ( + <> + + {t('errorStack')} + +
    +                {error.stack}
    +              
    + + ) : null} +
    + + + + + + +
    +
    + ); +}; + +export default ErrorPage; diff --git a/ui/pages/error/index.scss b/ui/pages/error/index.scss index 10e00597e8ed..b3d734899dd7 100644 --- a/ui/pages/error/index.scss +++ b/ui/pages/error/index.scss @@ -2,46 +2,40 @@ .error-page { display: flex; - flex-flow: column nowrap; - align-items: center; - font-style: normal; - font-weight: normal; - padding: 35px 10px 10px 10px; + flex-flow: column; + padding: 16px; height: 100%; - &__header { - @include design-system.H1; - - display: flex; - justify-content: center; - padding: 10px 0; - text-align: center; - } - - &__subheader { - @include design-system.H4; - - padding: 10px 0; - width: 100%; - max-width: 720px; - text-align: center; - } - - &__details { - @include design-system.H4; - - overflow-y: auto; - width: 100%; - max-width: 720px; - padding-top: 10px; + &__inner-wrapper { + @media screen and (min-width: design-system.$screen-md-min) { + width: 50%; + margin-left: auto; + margin-right: auto; + } } &__stack { - overflow-x: auto; - background-color: var(--color-background-alternative); + max-height: 120px; + overflow: auto; + font-family: var(--font-family-sans); + font-weight: var(--typography-l-body-xs-font-weight); + font-size: var(--typography-l-body-xs-font-size); } &__link-text { color: var(--color-primary-default); } } + +// override styles for sentry feedback form +#sentry-feedback { + --font-family: var(--font-family-sans); + --inset: auto 20px 20px auto; + --input-font-size: 14px; + --button-color: var(--color-primary-default); + --button-primary-background: var(--color-primary-default); + --button-primary-border: var(--color-primary-default); + --button-primary-hover-background: var(--color-primary-default); + --success-color: var(--color-success-default); + --error-color: var(--color-error-default); +} diff --git a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap index 4eea2d9cf7d1..17899d68724f 100644 --- a/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap +++ b/ui/pages/settings/developer-options-tab/__snapshots__/developer-options-tab.test.tsx.snap @@ -319,7 +319,7 @@ exports[`Develop options tab should match snapshot 1`] = ` class="settings-page__content-item-col" > + +
    +
    +
    +
    + diff --git a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx index a88d735a628f..a604082586fc 100644 --- a/ui/pages/settings/developer-options-tab/developer-options-tab.tsx +++ b/ui/pages/settings/developer-options-tab/developer-options-tab.tsx @@ -38,7 +38,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { getIsRedesignedConfirmationsDeveloperEnabled } from '../../confirmations/selectors/confirm'; import ToggleRow from './developer-options-toggle-row-component'; -import { SentryTest } from './sentry-test'; +import SentryTest from './sentry-test'; import { ProfileSyncDevSettings } from './profile-sync'; /** diff --git a/ui/pages/settings/developer-options-tab/sentry-test.tsx b/ui/pages/settings/developer-options-tab/sentry-test.tsx index 7d04e3a4dd87..5c7132a7cfea 100644 --- a/ui/pages/settings/developer-options-tab/sentry-test.tsx +++ b/ui/pages/settings/developer-options-tab/sentry-test.tsx @@ -1,8 +1,9 @@ import React, { useState, useCallback, ReactElement } from 'react'; -import { ButtonVariant } from '@metamask/snaps-sdk'; +import { useDispatch, useSelector } from 'react-redux'; import { Box, Button, + ButtonVariant, Icon, IconName, IconSize, @@ -16,8 +17,23 @@ import { JustifyContent, } from '../../../helpers/constants/design-system'; import { trace, TraceName } from '../../../../shared/lib/trace'; +import { ButtonSize } from '../../../components/component-library/button/button.types'; + +import { + forceUpdateMetamaskState, + setCurrentLocale, +} from '../../../store/actions'; +import { FALLBACK_LOCALE, fetchLocale } from '../../../../shared/modules/i18n'; +import { getCurrentLocale } from '../../../ducks/locale/locale'; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const SentryTest = () => { + const currentLocale: string = + useSelector(getCurrentLocale) || FALLBACK_LOCALE; -export function SentryTest() { return ( <> @@ -27,10 +43,11 @@ export function SentryTest() { + ); -} +}; function GenerateUIError() { const handleClick = useCallback(async () => { @@ -115,16 +132,48 @@ function GenerateTrace() { ); } +function GeneratePageCrash({ currentLocale }: { currentLocale: string }) { + const dispatch = useDispatch(); + const handleClick = async () => { + const localeMessages = await fetchLocale(currentLocale); + await dispatch( + setCurrentLocale(currentLocale, { + ...localeMessages, + // @ts-expect-error - remove a language string in this page to trigger a page crash + developerOptions: undefined, + }), + ); + await forceUpdateMetamaskState(dispatch); + }; + + return ( + + Trigger the crash on extension to send user feedback to sentry. You + can click "Try again" to reload extension + + } + onClick={handleClick} + expectError + testId="developer-options-generate-page-crash-button" + /> + ); +} + function TestButton({ name, description, onClick, expectError, + testId, }: { name: string; description: ReactElement; onClick: () => Promise; expectError?: boolean; + testId?: string; }) { const [isComplete, setIsComplete] = useState(false); @@ -155,7 +204,12 @@ function TestButton({
    {description}
    -
    @@ -180,6 +234,4 @@ function TestButton({ ); } -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} +export default SentryTest; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index 2f33cd78f2e1..35fe93c858d9 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -326,7 +326,7 @@ class SettingsPage extends PureComponent { }, ]; - if (process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) { + if (process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS || process.env.IN_TEST) { tabs.splice(-1, 0, { content: t('developerOptions'), icon: , @@ -395,7 +395,8 @@ class SettingsPage extends PureComponent { /> - {process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS && ( + {(process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS || + process.env.IN_TEST) && (