From 58382b7965c0b7be4a3cfa5a2fd0d94936a5c4b0 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Tue, 19 Apr 2022 02:42:54 +0200 Subject: feat(renderer): Error pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display an error page when page loading fails. Error messages should be tweaked for messaging applications (not browsers), because people won't neccessarily expect browser errors in a messenger. We still need a component to property show a certificate that failed validation so people can decide whether to trust it temporarily. Signed-off-by: Kristóf Marussy --- packages/renderer/src/components/App.tsx | 14 +- .../src/components/BrowserViewPlaceholder.tsx | 2 +- .../components/errorPage/CertificateDetails.tsx | 92 +++++++++++ .../src/components/errorPage/ErrorPage.tsx | 180 +++++++++++++++++++++ 4 files changed, 275 insertions(+), 13 deletions(-) create mode 100644 packages/renderer/src/components/errorPage/CertificateDetails.tsx create mode 100644 packages/renderer/src/components/errorPage/ErrorPage.tsx (limited to 'packages/renderer') diff --git a/packages/renderer/src/components/App.tsx b/packages/renderer/src/components/App.tsx index c3f83ee..26e2e01 100644 --- a/packages/renderer/src/components/App.tsx +++ b/packages/renderer/src/components/App.tsx @@ -19,7 +19,6 @@ */ import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import { observer } from 'mobx-react-lite'; import React, { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -27,6 +26,7 @@ import { useTranslation } from 'react-i18next'; import BrowserViewPlaceholder from './BrowserViewPlaceholder'; import NewWindowBanner from './NewWindowBanner'; import { useStore } from './StoreProvider'; +import ErrorPage from './errorPage/ErrorPage'; import LocationBar from './locationBar/LocationBar'; import Sidebar from './sidebar/Sidebar'; @@ -104,17 +104,7 @@ function App({ devMode }: { devMode: boolean }): JSX.Element { -

{JSON.stringify(selectedService?.state)}

- {selectedService?.state.type === 'certificateError' && ( - - )} +
diff --git a/packages/renderer/src/components/BrowserViewPlaceholder.tsx b/packages/renderer/src/components/BrowserViewPlaceholder.tsx index 1f5f9f4..9bd1176 100644 --- a/packages/renderer/src/components/BrowserViewPlaceholder.tsx +++ b/packages/renderer/src/components/BrowserViewPlaceholder.tsx @@ -65,7 +65,7 @@ function BrowserViewPlaceholder({ ); return ( - + {children} ); diff --git a/packages/renderer/src/components/errorPage/CertificateDetails.tsx b/packages/renderer/src/components/errorPage/CertificateDetails.tsx new file mode 100644 index 0000000..044cb5c --- /dev/null +++ b/packages/renderer/src/components/errorPage/CertificateDetails.tsx @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import type Service from '../../stores/Service'; + +const SUMMARY_ID = 'Sophie-CertificateDetails-header'; +const DETAILS_ID = 'Sophie-CertificateDetails-content'; + +function CertificateDetails({ + service, +}: { + service: Service; +}): JSX.Element | null { + const { t } = useTranslation(undefined, { + keyPrefix: 'error.certificateError.details', + }); + + const { state } = service; + const { type: errorType } = state; + + if (errorType !== 'certificateError') { + // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. + return null; + } + + const { certificate, trust } = state; + + return ( + + } + > + {t('title')} + + + + {JSON.stringify(certificate)} + + + {t('warning')} + + + + + + + ); +} + +export default observer(CertificateDetails); diff --git a/packages/renderer/src/components/errorPage/ErrorPage.tsx b/packages/renderer/src/components/errorPage/ErrorPage.tsx new file mode 100644 index 0000000..571059a --- /dev/null +++ b/packages/renderer/src/components/errorPage/ErrorPage.tsx @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import NoEncryptionIcon from '@mui/icons-material/NoEncryptionOutlined'; +import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import type { TFunction } from 'i18next'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import type Service from '../../stores/Service'; + +import CertificateDetails from './CertificateDetails'; + +interface ErrorDetails { + icon: JSX.Element; + title: string; + description: string; + errorCode: string | undefined; +} + +function formatError(service: Service, t: TFunction): ErrorDetails { + const { + settings: { name: serviceName }, + state, + } = service; + const { type } = state; + switch (type) { + case 'failed': { + const { errorCode, errorDesc } = state; + const errorCodeStr = errorCode.toString(10); + return { + icon: , + title: t( + [ + `error.failed.${errorCodeStr}.title`, + 'error.failed.unspecific.title', + ], + { serviceName }, + ), + description: t( + [ + `error.failed.${errorCodeStr}.description`, + 'error.failed.unspecific.description', + ], + { serviceName }, + ), + errorCode: + errorDesc.length > 0 + ? `${errorDesc} (${errorCodeStr})` + : errorCodeStr, + }; + } + case 'certificateError': { + const { errorCode } = state; + // Avoid i18next namespace separators in the error code. + const errorCodeSafe = errorCode.replaceAll(':', '_'); + return { + icon: , + title: t( + [ + `error.certificateError.${errorCodeSafe}.title`, + 'error.certificateError.unspecific.title', + ], + { serviceName }, + ), + description: t( + [ + `error.certificateError.${errorCodeSafe}.description`, + 'error.certificateError.unspecific.description', + ], + { serviceName }, + ), + errorCode, + }; + } + case 'crashed': { + const { reason, exitCode } = state; + return { + icon: , + title: t( + [`error.crashed.${reason}.title`, 'error.crashed.unspecific.title'], + { serviceName }, + ), + description: t( + [ + `error.crashed.${reason}.description`, + 'error.crashed.unspecific.description', + ], + { serviceName }, + ), + errorCode: `${reason} (${exitCode})`, + }; + } + default: + return { + icon: , + title: t('error.unknown.title', { serviceName }), + description: t('error.unknown.description', { serviceName }), + errorCode: undefined, + }; + } +} + +function ErrorPage({ + service, +}: { + service: Service | undefined; +}): JSX.Element | null { + const { t } = useTranslation(undefined); + + if (service === undefined || !service.hasError) { + // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. + return null; + } + + const { + settings: { name: serviceName }, + state: { type: errorType }, + } = service; + const { icon, title, description, errorCode } = formatError(service, t); + + return ( + + + + {icon} + + + {title} + + {description} + {errorCode !== undefined && ( + + {t('error.errorCode', { errorCode })} + + )} + + + {errorType !== 'certificateError' && ( + + )} + + + + + ); +} + +export default observer(ErrorPage); -- cgit v1.2.3-54-g00ecf