Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / support / BugModal.tsx
blobbe3bec817f5142f35d0c43ceb7b2f91f27f49264
1 import { useEffect, useMemo, useState } from 'react';
2 import { useLocation } from 'react-router-dom';
4 import { c } from 'ttag';
6 import { Button, Href } from '@proton/atoms';
7 import Collapsible from '@proton/components/components/collapsible/Collapsible';
8 import CollapsibleContent from '@proton/components/components/collapsible/CollapsibleContent';
9 import CollapsibleHeader from '@proton/components/components/collapsible/CollapsibleHeader';
10 import CollapsibleHeaderIconButton from '@proton/components/components/collapsible/CollapsibleHeaderIconButton';
11 import { DropdownSizeUnit } from '@proton/components/components/dropdown/utils';
12 import Form from '@proton/components/components/form/Form';
13 import Icon from '@proton/components/components/icon/Icon';
14 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
15 import Modal from '@proton/components/components/modalTwo/Modal';
16 import ModalContent from '@proton/components/components/modalTwo/ModalContent';
17 import ModalFooter from '@proton/components/components/modalTwo/ModalFooter';
18 import ModalHeader from '@proton/components/components/modalTwo/ModalHeader';
19 import Option from '@proton/components/components/option/Option';
20 import SelectTwo from '@proton/components/components/selectTwo/SelectTwo';
21 import InputFieldTwo from '@proton/components/components/v2/field/InputField';
22 import TextAreaTwo from '@proton/components/components/v2/input/TextArea';
23 import useFormErrors from '@proton/components/components/v2/useFormErrors';
24 import useApi from '@proton/components/hooks/useApi';
25 import useConfig from '@proton/components/hooks/useConfig';
26 import useNotifications from '@proton/components/hooks/useNotifications';
27 import { reportBug } from '@proton/shared/lib/api/reports';
28 import type { APP_NAMES } from '@proton/shared/lib/constants';
29 import { APPS, BRAND_NAME, CLIENT_TYPES } from '@proton/shared/lib/constants';
30 import { requiredValidator } from '@proton/shared/lib/helpers/formValidators';
31 import { omit } from '@proton/shared/lib/helpers/object';
32 import { getKnowledgeBaseUrl } from '@proton/shared/lib/helpers/url';
33 import isTruthy from '@proton/utils/isTruthy';
34 import noop from '@proton/utils/noop';
36 import { getClientName, getReportInfo } from '../../helpers/report';
37 import type { Screenshot } from './AttachScreenshot';
38 import AttachScreenshot from './AttachScreenshot';
40 export type BugModalMode = 'chat-no-agents';
42 type OptionLabelItem = { type: 'label'; value: string };
43 type OptionOptionItem = {
44     type: 'option';
45     title: string;
46     value: string;
47     clientType?: CLIENT_TYPES;
48     app?: APP_NAMES;
50 type OptionItem = OptionOptionItem | OptionLabelItem;
52 interface Model extends ReturnType<typeof getReportInfo> {
53     Category: OptionOptionItem | undefined;
54     Description: string;
55     Email: string;
56     Username: string;
59 export interface Props {
60     username?: string;
61     email?: string;
62     mode?: BugModalMode;
63     onClose: ModalProps['onClose'];
64     onExit: ModalProps['onExit'];
65     open: ModalProps['open'];
66     app?: APP_NAMES;
69 const getMailOptions = (): OptionItem[] => {
70     const optionType = 'option' as const;
71     const labelType = 'label' as const;
72     return [
73         { type: labelType, value: c('Group').t`Account` },
74         { type: optionType, value: 'Sign in problem', title: c('Bug category').t`Sign in problem` },
75         { type: optionType, value: 'Sign up problem', title: c('Bug category').t`Sign up problem` },
76         { type: optionType, value: 'Payments problem', title: c('Bug category').t`Payments problem` },
77         { type: optionType, value: 'Custom domain problem', title: c('Bug category').t`Custom domain problem` },
78         { type: labelType, value: c('Group').t`Apps` },
79         { type: optionType, value: 'Bridge problem', title: c('Bug category').t`Bridge problem` },
80         { type: optionType, value: 'Import / export problem', title: c('Bug category').t`Import / export problem` },
81         { type: labelType, value: c('Group').t`Network` },
82         { type: optionType, value: 'Connection problem', title: c('Bug category').t`Connection problem` },
83         { type: optionType, value: 'Slow speed problem', title: c('Bug category').t`Slow speed problem` },
84         { type: labelType, value: c('Group').t`Services` },
85         {
86             type: optionType,
87             value: 'Calendar problem',
88             title: c('Bug category').t`Calendar problem`,
89             app: APPS.PROTONCALENDAR,
90         },
91         { type: optionType, value: 'Contacts problem', title: c('Bug category').t`Contacts problem` },
92         {
93             type: optionType,
94             value: 'Drive problem',
95             title: c('Bug category').t`Drive problem`,
96             app: APPS.PROTONDRIVE,
97         },
98         {
99             type: optionType,
100             value: 'Docs problem',
101             title: c('Bug category').t`Docs problem`,
102             app: APPS.PROTONDOCS,
103         },
104         {
105             type: optionType,
106             value: 'Mail problem',
107             title: c('Bug category').t`Mail problem`,
108             app: APPS.PROTONMAIL,
109         },
110         {
111             type: optionType,
112             value: 'VPN problem',
113             title: c('Bug category').t`VPN problem`,
114             clientType: CLIENT_TYPES.VPN,
115             app: APPS.PROTONVPN_SETTINGS,
116         },
117         {
118             type: optionType,
119             value: 'Pass problem',
120             title: c('Bug category').t`Pass problem`,
121             clientType: CLIENT_TYPES.PASS,
122             app: APPS.PROTONPASS,
123         },
124         {
125             type: optionType,
126             value: 'Wallet problem',
127             title: c('wallet_signup_2024:Bug category').t`Wallet problem`,
128             clientType: CLIENT_TYPES.WALLET,
129             app: APPS.PROTONWALLET,
130         },
131         { type: labelType, value: c('Group').t`Other category` },
132         { type: optionType, value: 'Feature request', title: c('Bug category').t`Feature request` },
133         { type: optionType, value: 'Other', title: c('Bug category').t`Other` },
134     ].filter(isTruthy);
137 const getVPNOptions = (): OptionItem[] => {
138     return [
139         { type: 'option', value: 'Login problem', title: c('Bug category').t`Sign in problem` },
140         { type: 'option', value: 'Signup problem', title: c('Bug category').t`Signup problem` },
141         { type: 'option', value: 'Payments problem', title: c('Bug category').t`Payments problem` },
142         { type: 'option', value: 'Installation problem', title: c('Bug category').t`Installation problem` },
143         { type: 'option', value: 'Update problem', title: c('Bug category').t`Update problem` },
144         { type: 'option', value: 'Application problem', title: c('Bug category').t`Application problem` },
145         { type: 'option', value: 'Connection problem', title: c('Bug category').t`Connection problem` },
146         { type: 'option', value: 'Speed problem', title: c('Bug category').t`Speed problem` },
147         { type: 'option', value: 'Manual setup problem', title: c('Bug category').t`Manual setup problem` },
148         { type: 'option', value: 'Website access problem', title: c('Bug category').t`Website access problem` },
149         { type: 'option', value: 'Streaming problem', title: c('Bug category').t`Streaming problem` },
150         { type: 'option', value: 'Feature request', title: c('Bug category').t`Feature request` },
151     ];
154 const BugModal = ({ username: Username = '', email, mode, open, onClose, onExit, app: maybeApp }: Props) => {
155     const api = useApi();
156     const location = useLocation();
157     const [loading, setLoading] = useState(false);
159     const handleClose = loading ? noop : onClose;
161     const { APP_VERSION, CLIENT_TYPE, APP_NAME } = useConfig();
162     const isVpn = APP_NAME === APPS.PROTONVPN_SETTINGS;
163     const isDrive = APP_NAME === APPS.PROTONDRIVE;
164     const clearCacheLink = isVpn
165         ? 'https://protonvpn.com/support/clear-browser-cache-cookies/'
166         : getKnowledgeBaseUrl('/how-to-clean-cache-and-cookies');
167     const Client = getClientName(APP_NAME);
168     const showCategory = !isDrive;
169     const { createNotification } = useNotifications();
171     const options = useMemo(() => {
172         return isVpn ? getVPNOptions() : getMailOptions();
173     }, []);
175     const categoryOptions = options.map((option) => {
176         const { type, value } = option;
177         if (type === 'label') {
178             return (
179                 <label className="text-semibold px-2 py-1 block" key={value}>
180                     {value}
181                 </label>
182             );
183         }
185         const { title } = option;
186         return <Option title={title} value={option} key={value} />;
187     });
189     const [model, setModel] = useState<Model>(() => {
190         const app = maybeApp || APP_NAME;
191         const defaultCategory = options.find(
192             (option): option is OptionOptionItem => option.type === 'option' && option.app === app
193         );
194         return {
195             ...getReportInfo(),
196             Category: defaultCategory,
197             Description: '',
198             Email: email || '',
199             Username: Username || '',
200         };
201     });
202     const [screenshots, setScreenshots] = useState<Screenshot[]>([]);
203     const [uploadingScreenshots, setUploadingScreenshots] = useState(false);
204     const link = <Href key="linkClearCache" href={clearCacheLink}>{c('Link').t`clearing your browser cache`}</Href>;
206     const { validator, onFormSubmit } = useFormErrors();
208     const setModelDiff = (model: Partial<Model>) => {
209         setModel((oldModel) => ({ ...oldModel, ...model }));
210     };
211     const handleChange = <K extends keyof Model>(key: K) => {
212         return (value: Model[K]) => setModelDiff({ [key]: value });
213     };
215     const categoryTitle = model.Category?.title || '';
216     const clientType = model.Category?.clientType || CLIENT_TYPE;
218     const handleSubmit = async () => {
219         if (!onFormSubmit()) {
220             return;
221         }
223         setLoading(true);
225         const getParameters = () => {
226             const screenshotBlobs = screenshots.reduce((acc: { [key: string]: Blob }, { name, blob }) => {
227                 acc[name] = blob;
228                 return acc;
229             }, {});
231             const Title = [!isVpn && '[V5]', `[${Client}] Bug [${location.pathname}]`, categoryTitle]
232                 .filter(Boolean)
233                 .join(' ');
235             return {
236                 ...screenshotBlobs,
237                 ...omit(model, ['OSArtificial', 'Category']),
238                 Trigger: mode || '',
239                 Client,
240                 ClientVersion: APP_VERSION,
241                 ClientType: clientType,
242                 Title,
243             };
244         };
246         try {
247             await api(reportBug(getParameters(), 'form'));
248             onClose?.();
249             createNotification({ text: c('Success').t`Problem reported` });
250         } catch (error) {
251             setLoading(false);
252         }
253     };
255     useEffect(() => {
256         if (!model.Email && email) {
257             setModel({ ...model, Email: email });
258         }
259     }, [email]);
261     const OSAndOSVersionFields = (
262         <>
263             <InputFieldTwo
264                 id="OS"
265                 label={c('Label').t`Operating system`}
266                 value={model.OS}
267                 onValue={handleChange('OS')}
268                 disabled={loading}
269             />
270             <InputFieldTwo
271                 id="OSVersion"
272                 label={c('Label').t`Operating system version`}
273                 value={model.OSVersion}
274                 onValue={handleChange('OSVersion')}
275                 disabled={loading}
276             />
277         </>
278     );
280     const modeAlert = (() => {
281         if (mode === 'chat-no-agents') {
282             return (
283                 <p>
284                     {c('Warning')
285                         .t`Unfortunately, we’re not online at the moment. Please complete the form below to describe your issue, and we will look into it and be in touch when we’re back online.`}
286                 </p>
287             );
288         }
290         return (
291             <>
292                 <p>
293                     {c('Info').jt`Refreshing the page or ${link} will automatically resolve most issues.`}
294                     <br />
295                     <br />
296                     {c('Warning')
297                         .t`Reports are not end-to-end encrypted, please do not send any sensitive information.`}
298                 </p>
299             </>
300         );
301     })();
303     // Retrieves the selected option by title to ensure referential equality for Select's value
304     const selectedValue = options.find(
305         (option) => (option.type === 'option' && option.title === model.Category?.title) || ''
306     );
308     return (
309         <Modal as={Form} open={open} onClose={handleClose} onExit={onExit} onSubmit={handleSubmit}>
310             <ModalHeader title={c('Title').t`Report a problem`} />
311             <ModalContent>
312                 {modeAlert}
313                 {Username ? null : (
314                     <InputFieldTwo
315                         autoFocus
316                         id="Username"
317                         label={c('Label').t`${BRAND_NAME} username`}
318                         value={model.Username}
319                         onValue={handleChange('Username')}
320                         disabled={loading}
321                     />
322                 )}
323                 <InputFieldTwo
324                     id="Email"
325                     label={c('Label').t`Email address`}
326                     placeholder={c('Placeholder').t`A way to contact you`}
327                     value={model.Email}
328                     onValue={handleChange('Email')}
329                     error={validator([requiredValidator(model.Email)])}
330                     disabled={loading}
331                 />
332                 {showCategory && (
333                     <InputFieldTwo
334                         as={SelectTwo<OptionItem>}
335                         label={c('Label').t`Category`}
336                         placeholder={c('Placeholder').t`Select`}
337                         id="Title"
338                         value={selectedValue}
339                         onValue={(option: OptionItem) => {
340                             if (option.type === 'option') {
341                                 setModelDiff({ Category: option });
342                             }
343                         }}
344                         error={validator([requiredValidator(categoryTitle)])}
345                         disabled={loading}
346                         size={{
347                             width: DropdownSizeUnit.Anchor,
348                             maxWidth: DropdownSizeUnit.Viewport,
349                             maxHeight: '16em',
350                         }}
351                     >
352                         {categoryOptions}
353                     </InputFieldTwo>
354                 )}
355                 <InputFieldTwo
356                     as={TextAreaTwo}
357                     id="Description"
358                     label={c('Label').t`What happened?`}
359                     placeholder={c('Placeholder').t`Please describe the problem and include any error messages`}
360                     value={model.Description}
361                     onValue={handleChange('Description')}
362                     error={validator([requiredValidator(model.Description)])}
363                     rows={5}
364                     disabled={loading}
365                 />
366                 <AttachScreenshot
367                     id="Attachments"
368                     screenshots={screenshots}
369                     setScreenshots={setScreenshots}
370                     uploading={uploadingScreenshots}
371                     setUploading={setUploadingScreenshots}
372                     disabled={loading}
373                 />
374                 {model.OSArtificial && OSAndOSVersionFields}
376                 <Collapsible className="mt-4">
377                     <CollapsibleHeader
378                         disableFullWidth
379                         suffix={
380                             <CollapsibleHeaderIconButton size="small">
381                                 <Icon name="chevron-down" />
382                             </CollapsibleHeaderIconButton>
383                         }
384                     >
385                         <label className="text-semibold">{c('Label').t`System information`}</label>
386                     </CollapsibleHeader>
388                     <CollapsibleContent className="mt-4">
389                         {!model.OSArtificial && OSAndOSVersionFields}
391                         <InputFieldTwo
392                             id="Browser"
393                             label={c('Label').t`Browser`}
394                             value={model.Browser}
395                             onValue={handleChange('Browser')}
396                             disabled={loading}
397                         />
398                         <InputFieldTwo
399                             id="BrowserVersion"
400                             label={c('Label').t`Browser version`}
401                             value={model.BrowserVersion}
402                             onValue={handleChange('BrowserVersion')}
403                             disabled={loading}
404                         />
405                     </CollapsibleContent>
406                 </Collapsible>
407             </ModalContent>
408             <ModalFooter>
409                 <Button onClick={handleClose} disabled={loading}>
410                     {c('Action').t`Cancel`}
411                 </Button>
412                 <Button loading={loading} disabled={uploadingScreenshots} type="submit" color="norm">
413                     {c('Action').t`Submit`}
414                 </Button>
415             </ModalFooter>
416         </Modal>
417     );
420 export default BugModal;