diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-04-19 02:42:54 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-05-16 00:55:01 +0200 |
commit | 58382b7965c0b7be4a3cfa5a2fd0d94936a5c4b0 (patch) | |
tree | 30b5c20ce81f8d95d74cb513e6d01bedbae483a8 /packages/renderer/src | |
parent | fix(renderer): Make RTL flipping more resilient to hot reloading (diff) | |
download | sophie-58382b7965c0b7be4a3cfa5a2fd0d94936a5c4b0.tar.gz sophie-58382b7965c0b7be4a3cfa5a2fd0d94936a5c4b0.tar.zst sophie-58382b7965c0b7be4a3cfa5a2fd0d94936a5c4b0.zip |
feat(renderer): Error pages
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 <kristof@marussy.com>
Diffstat (limited to 'packages/renderer/src')
4 files changed, 275 insertions, 13 deletions
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 @@ | |||
19 | */ | 19 | */ |
20 | 20 | ||
21 | import Box from '@mui/material/Box'; | 21 | import Box from '@mui/material/Box'; |
22 | import Button from '@mui/material/Button'; | ||
23 | import { observer } from 'mobx-react-lite'; | 22 | import { observer } from 'mobx-react-lite'; |
24 | import React, { useCallback, useEffect } from 'react'; | 23 | import React, { useCallback, useEffect } from 'react'; |
25 | import { useTranslation } from 'react-i18next'; | 24 | import { useTranslation } from 'react-i18next'; |
@@ -27,6 +26,7 @@ import { useTranslation } from 'react-i18next'; | |||
27 | import BrowserViewPlaceholder from './BrowserViewPlaceholder'; | 26 | import BrowserViewPlaceholder from './BrowserViewPlaceholder'; |
28 | import NewWindowBanner from './NewWindowBanner'; | 27 | import NewWindowBanner from './NewWindowBanner'; |
29 | import { useStore } from './StoreProvider'; | 28 | import { useStore } from './StoreProvider'; |
29 | import ErrorPage from './errorPage/ErrorPage'; | ||
30 | import LocationBar from './locationBar/LocationBar'; | 30 | import LocationBar from './locationBar/LocationBar'; |
31 | import Sidebar from './sidebar/Sidebar'; | 31 | import Sidebar from './sidebar/Sidebar'; |
32 | 32 | ||
@@ -104,17 +104,7 @@ function App({ devMode }: { devMode: boolean }): JSX.Element { | |||
104 | <LocationBar /> | 104 | <LocationBar /> |
105 | <NewWindowBanner service={selectedService} /> | 105 | <NewWindowBanner service={selectedService} /> |
106 | <BrowserViewPlaceholder> | 106 | <BrowserViewPlaceholder> |
107 | <p>{JSON.stringify(selectedService?.state)}</p> | 107 | <ErrorPage service={selectedService} /> |
108 | {selectedService?.state.type === 'certificateError' && ( | ||
109 | <Button | ||
110 | disabled={selectedService.state.trust !== 'pending'} | ||
111 | onClick={() => | ||
112 | selectedService?.temporarilyTrustCurrentCertificate() | ||
113 | } | ||
114 | > | ||
115 | Trust certificate | ||
116 | </Button> | ||
117 | )} | ||
118 | </BrowserViewPlaceholder> | 108 | </BrowserViewPlaceholder> |
119 | </Box> | 109 | </Box> |
120 | </Box> | 110 | </Box> |
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({ | |||
65 | ); | 65 | ); |
66 | 66 | ||
67 | return ( | 67 | return ( |
68 | <Box flex={1} ref={ref}> | 68 | <Box ref={ref} flex={1} overflow="auto"> |
69 | {children} | 69 | {children} |
70 | </Box> | 70 | </Box> |
71 | ); | 71 | ); |
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 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | ||
22 | import WarningAmberIcon from '@mui/icons-material/WarningAmber'; | ||
23 | import Accordion from '@mui/material/Accordion'; | ||
24 | import AccordionDetails from '@mui/material/AccordionDetails'; | ||
25 | import AccordionSummary from '@mui/material/AccordionSummary'; | ||
26 | import Box from '@mui/material/Box'; | ||
27 | import Button from '@mui/material/Button'; | ||
28 | import Typography from '@mui/material/Typography'; | ||
29 | import { observer } from 'mobx-react-lite'; | ||
30 | import React from 'react'; | ||
31 | import { useTranslation } from 'react-i18next'; | ||
32 | |||
33 | import type Service from '../../stores/Service'; | ||
34 | |||
35 | const SUMMARY_ID = 'Sophie-CertificateDetails-header'; | ||
36 | const DETAILS_ID = 'Sophie-CertificateDetails-content'; | ||
37 | |||
38 | function CertificateDetails({ | ||
39 | service, | ||
40 | }: { | ||
41 | service: Service; | ||
42 | }): JSX.Element | null { | ||
43 | const { t } = useTranslation(undefined, { | ||
44 | keyPrefix: 'error.certificateError.details', | ||
45 | }); | ||
46 | |||
47 | const { state } = service; | ||
48 | const { type: errorType } = state; | ||
49 | |||
50 | if (errorType !== 'certificateError') { | ||
51 | // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. | ||
52 | return null; | ||
53 | } | ||
54 | |||
55 | const { certificate, trust } = state; | ||
56 | |||
57 | return ( | ||
58 | <Accordion | ||
59 | disableGutters | ||
60 | variant="outlined" | ||
61 | sx={{ '&::before': { display: 'none' }, borderRadius: 1 }} | ||
62 | > | ||
63 | <AccordionSummary | ||
64 | id={SUMMARY_ID} | ||
65 | aria-controls={DETAILS_ID} | ||
66 | expandIcon={<ExpandMoreIcon />} | ||
67 | > | ||
68 | <Typography>{t('title')}</Typography> | ||
69 | </AccordionSummary> | ||
70 | <AccordionDetails> | ||
71 | <Typography sx={{ wordWrap: 'break-word' }}> | ||
72 | {JSON.stringify(certificate)} | ||
73 | </Typography> | ||
74 | <Typography mt={2}> | ||
75 | <WarningAmberIcon fontSize="small" /> <strong>{t('warning')}</strong> | ||
76 | </Typography> | ||
77 | <Box mt={1}> | ||
78 | <Button | ||
79 | disabled={trust !== 'pending'} | ||
80 | onClick={() => service.temporarilyTrustCurrentCertificate()} | ||
81 | color="error" | ||
82 | variant="outlined" | ||
83 | > | ||
84 | {t(`acceptButton.${trust}`)} | ||
85 | </Button> | ||
86 | </Box> | ||
87 | </AccordionDetails> | ||
88 | </Accordion> | ||
89 | ); | ||
90 | } | ||
91 | |||
92 | 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 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; | ||
22 | import NoEncryptionIcon from '@mui/icons-material/NoEncryptionOutlined'; | ||
23 | import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; | ||
24 | import Box from '@mui/material/Box'; | ||
25 | import Button from '@mui/material/Button'; | ||
26 | import Typography from '@mui/material/Typography'; | ||
27 | import type { TFunction } from 'i18next'; | ||
28 | import { observer } from 'mobx-react-lite'; | ||
29 | import React from 'react'; | ||
30 | import { useTranslation } from 'react-i18next'; | ||
31 | |||
32 | import type Service from '../../stores/Service'; | ||
33 | |||
34 | import CertificateDetails from './CertificateDetails'; | ||
35 | |||
36 | interface ErrorDetails { | ||
37 | icon: JSX.Element; | ||
38 | title: string; | ||
39 | description: string; | ||
40 | errorCode: string | undefined; | ||
41 | } | ||
42 | |||
43 | function formatError(service: Service, t: TFunction): ErrorDetails { | ||
44 | const { | ||
45 | settings: { name: serviceName }, | ||
46 | state, | ||
47 | } = service; | ||
48 | const { type } = state; | ||
49 | switch (type) { | ||
50 | case 'failed': { | ||
51 | const { errorCode, errorDesc } = state; | ||
52 | const errorCodeStr = errorCode.toString(10); | ||
53 | return { | ||
54 | icon: <ErrorOutlineIcon fontSize="inherit" />, | ||
55 | title: t( | ||
56 | [ | ||
57 | `error.failed.${errorCodeStr}.title`, | ||
58 | 'error.failed.unspecific.title', | ||
59 | ], | ||
60 | { serviceName }, | ||
61 | ), | ||
62 | description: t( | ||
63 | [ | ||
64 | `error.failed.${errorCodeStr}.description`, | ||
65 | 'error.failed.unspecific.description', | ||
66 | ], | ||
67 | { serviceName }, | ||
68 | ), | ||
69 | errorCode: | ||
70 | errorDesc.length > 0 | ||
71 | ? `${errorDesc} (${errorCodeStr})` | ||
72 | : errorCodeStr, | ||
73 | }; | ||
74 | } | ||
75 | case 'certificateError': { | ||
76 | const { errorCode } = state; | ||
77 | // Avoid i18next namespace separators in the error code. | ||
78 | const errorCodeSafe = errorCode.replaceAll(':', '_'); | ||
79 | return { | ||
80 | icon: <NoEncryptionIcon fontSize="inherit" />, | ||
81 | title: t( | ||
82 | [ | ||
83 | `error.certificateError.${errorCodeSafe}.title`, | ||
84 | 'error.certificateError.unspecific.title', | ||
85 | ], | ||
86 | { serviceName }, | ||
87 | ), | ||
88 | description: t( | ||
89 | [ | ||
90 | `error.certificateError.${errorCodeSafe}.description`, | ||
91 | 'error.certificateError.unspecific.description', | ||
92 | ], | ||
93 | { serviceName }, | ||
94 | ), | ||
95 | errorCode, | ||
96 | }; | ||
97 | } | ||
98 | case 'crashed': { | ||
99 | const { reason, exitCode } = state; | ||
100 | return { | ||
101 | icon: <SentimentVeryDissatisfiedIcon fontSize="inherit" />, | ||
102 | title: t( | ||
103 | [`error.crashed.${reason}.title`, 'error.crashed.unspecific.title'], | ||
104 | { serviceName }, | ||
105 | ), | ||
106 | description: t( | ||
107 | [ | ||
108 | `error.crashed.${reason}.description`, | ||
109 | 'error.crashed.unspecific.description', | ||
110 | ], | ||
111 | { serviceName }, | ||
112 | ), | ||
113 | errorCode: `${reason} (${exitCode})`, | ||
114 | }; | ||
115 | } | ||
116 | default: | ||
117 | return { | ||
118 | icon: <ErrorOutlineIcon fontSize="inherit" />, | ||
119 | title: t('error.unknown.title', { serviceName }), | ||
120 | description: t('error.unknown.description', { serviceName }), | ||
121 | errorCode: undefined, | ||
122 | }; | ||
123 | } | ||
124 | } | ||
125 | |||
126 | function ErrorPage({ | ||
127 | service, | ||
128 | }: { | ||
129 | service: Service | undefined; | ||
130 | }): JSX.Element | null { | ||
131 | const { t } = useTranslation(undefined); | ||
132 | |||
133 | if (service === undefined || !service.hasError) { | ||
134 | // eslint-disable-next-line unicorn/no-null -- React requires `null` to skip rendering. | ||
135 | return null; | ||
136 | } | ||
137 | |||
138 | const { | ||
139 | settings: { name: serviceName }, | ||
140 | state: { type: errorType }, | ||
141 | } = service; | ||
142 | const { icon, title, description, errorCode } = formatError(service, t); | ||
143 | |||
144 | return ( | ||
145 | <Box p={2}> | ||
146 | <Box component="section" maxWidth={800} mx="auto" my={{ md: '10vh' }}> | ||
147 | <Typography | ||
148 | aria-hidden | ||
149 | fontSize={{ xs: 48, md: 96 }} | ||
150 | lineHeight={1} | ||
151 | mb={2} | ||
152 | > | ||
153 | {icon} | ||
154 | </Typography> | ||
155 | <Typography component="h1" variant="h3" mb={2}> | ||
156 | {title} | ||
157 | </Typography> | ||
158 | <Typography mb={4}>{description}</Typography> | ||
159 | {errorCode !== undefined && ( | ||
160 | <Typography variant="body2" color="text.secondary" mb={2}> | ||
161 | {t('error.errorCode', { errorCode })} | ||
162 | </Typography> | ||
163 | )} | ||
164 | <Box display="flex" flexDirection="row" gap={1} mb={2}> | ||
165 | <Button onClick={() => service.goHome()} variant="contained"> | ||
166 | {t('error.home', { serviceName })} | ||
167 | </Button> | ||
168 | {errorType !== 'certificateError' && ( | ||
169 | <Button onClick={() => service.reload()} variant="outlined"> | ||
170 | {t('error.reload')} | ||
171 | </Button> | ||
172 | )} | ||
173 | </Box> | ||
174 | <CertificateDetails service={service} /> | ||
175 | </Box> | ||
176 | </Box> | ||
177 | ); | ||
178 | } | ||
179 | |||
180 | export default observer(ErrorPage); | ||