Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / contacts / email / ContactEmailSettingsModal.tsx
blob628d8b07be5924cc7d99c71a7db64e378709b245
1 import { useEffect, useState } from 'react';
3 import { c } from 'ttag';
5 import { Button } from '@proton/atoms';
6 import Alert from '@proton/components/components/alert/Alert';
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 Field from '@proton/components/components/container/Field';
12 import Row from '@proton/components/components/container/Row';
13 import Icon from '@proton/components/components/icon/Icon';
14 import Label from '@proton/components/components/label/Label';
15 import Info from '@proton/components/components/link/Info';
16 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
17 import ModalTwo from '@proton/components/components/modalTwo/Modal';
18 import ModalTwoContent from '@proton/components/components/modalTwo/ModalContent';
19 import ModalTwoFooter from '@proton/components/components/modalTwo/ModalFooter';
20 import ModalTwoHeader from '@proton/components/components/modalTwo/ModalHeader';
21 import useApi from '@proton/components/hooks/useApi';
22 import useEventManager from '@proton/components/hooks/useEventManager';
23 import useNotifications from '@proton/components/hooks/useNotifications';
24 import { useLoading } from '@proton/hooks';
25 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
26 import getPublicKeysEmailHelper from '@proton/shared/lib/api/helpers/getPublicKeysEmailHelper';
27 import { extractScheme } from '@proton/shared/lib/api/helpers/mailSettings';
28 import type { CONTACT_MIME_TYPES } from '@proton/shared/lib/constants';
29 import { MIME_TYPES, MIME_TYPES_MORE, PGP_SCHEMES } from '@proton/shared/lib/constants';
30 import { VCARD_KEY_FIELDS } from '@proton/shared/lib/contacts/constants';
31 import { getKeyInfoFromProperties, getMimeTypeVcard, toKeyProperty } from '@proton/shared/lib/contacts/keyProperties';
32 import {
33     createContactPropertyUid,
34     fromVCardProperties,
35     getVCardProperties,
36 } from '@proton/shared/lib/contacts/properties';
37 import type { ContactPublicKeyModelWithApiKeySource } from '@proton/shared/lib/interfaces';
38 import type { VCardContact, VCardProperty } from '@proton/shared/lib/interfaces/contacts/VCard';
39 import {
40     getContactPublicKeyModel,
41     getVerifyingKeys,
42     sortApiKeys,
43     sortPinnedKeys,
44 } from '@proton/shared/lib/keys/publicKeys';
45 import clsx from '@proton/utils/clsx';
46 import uniqueBy from '@proton/utils/uniqueBy';
48 import { useKeyTransparencyContext } from '../../keyTransparency/useKeyTransparencyContext';
49 import { useSaveVCardContact } from '../hooks/useSaveVCardContact';
50 import ContactMIMETypeSelect from './ContactMIMETypeSelect';
51 import ContactPGPSettings from './ContactPGPSettings';
53 const { PGP_INLINE } = PGP_SCHEMES;
55 export interface ContactEmailSettingsProps {
56     contactID: string;
57     vCardContact: VCardContact;
58     emailProperty: VCardProperty<string>;
59     onClose?: () => void;
62 type Props = ContactEmailSettingsProps & ModalProps;
64 const ContactEmailSettingsModal = ({ contactID, vCardContact, emailProperty, ...rest }: Props) => {
65     const { value: emailAddressValue, group: emailGroup } = emailProperty;
66     const emailAddress = emailAddressValue as string;
68     const api = useApi();
69     const { call } = useEventManager();
70     const [model, setModel] = useState<ContactPublicKeyModelWithApiKeySource>();
71     const [showPgpSettings, setShowPgpSettings] = useState(false);
72     const [loadingPgpSettings, withLoadingPgpSettings] = useLoading(true);
73     const [loadingSave, withLoadingSave] = useLoading(false);
74     const { createNotification } = useNotifications();
75     const [mailSettings] = useMailSettings();
76     const { verifyOutboundPublicKeys, ktActivation } = useKeyTransparencyContext();
78     const saveVCardContact = useSaveVCardContact();
80     // Avoid nested ternary
81     let isMimeTypeFixed: boolean;
82     if (model?.isPGPInternal) {
83         isMimeTypeFixed = false;
84     } else {
85         isMimeTypeFixed = model?.sign !== undefined ? model.sign : !!mailSettings?.Sign;
86     }
88     const hasPGPInline = model && mailSettings ? extractScheme(model, mailSettings) === PGP_INLINE : false;
90     /**
91      * Initialize the key model for the modal
92      */
93     const prepare = async () => {
94         const apiKeysConfig = await getPublicKeysEmailHelper({
95             email: emailAddress,
96             includeInternalKeysWithE2EEDisabledForMail: true, // the keys are used in the context of calendar sharing, thus users may want to pin them
97             api,
98             ktActivation,
99             verifyOutboundPublicKeys,
100             silence: true,
101         });
102         const apiKeysSourceMap = apiKeysConfig.publicKeys.reduce<
103             ContactPublicKeyModelWithApiKeySource['apiKeysSourceMap']
104         >((map, { publicKey, source }) => {
105             const fingerprint = publicKey.getFingerprint();
106             if (!map[source]) {
107                 map[source] = new Set();
108             }
109             map[source]!.add(fingerprint);
110             return map;
111         }, {});
112         const pinnedKeysConfig = await getKeyInfoFromProperties(vCardContact, emailGroup || '');
113         const publicKeyModel = await getContactPublicKeyModel({
114             emailAddress,
115             apiKeysConfig,
116             pinnedKeysConfig: { ...pinnedKeysConfig, isContact: true },
117         });
118         setModel({
119             ...publicKeyModel,
120             // Encryption enforces signing, so we can ignore the signing preference so that if the user
121             // disables encryption, the global default signing setting is automatically selected.
122             sign: publicKeyModel.encrypt ? undefined : publicKeyModel.sign,
123             apiKeysSourceMap,
124         });
125     };
127     /**
128      * Collect keys from the model to save
129      * @param group attached to the current email address
130      * @returns key properties to save in the vCard
131      */
132     const getKeysProperties = (group: string, model: ContactPublicKeyModelWithApiKeySource) => {
133         const allKeys = model?.isPGPInternal
134             ? [...model.publicKeys.apiKeys]
135             : [...model.publicKeys?.apiKeys, ...model.publicKeys.pinnedKeys];
136         const trustedKeys = allKeys.filter((publicKey) => model.trustedFingerprints.has(publicKey.getFingerprint()));
137         const uniqueTrustedKeys = uniqueBy(trustedKeys, (publicKey) => publicKey.getFingerprint());
138         return Promise.all(uniqueTrustedKeys.map((publicKey, index) => toKeyProperty({ publicKey, group, index })));
139     };
141     /**
142      * Save relevant key properties in the vCard
143      */
144     const handleSubmit = async (model?: ContactPublicKeyModelWithApiKeySource) => {
145         if (!model) {
146             return;
147         }
148         const properties = getVCardProperties(vCardContact);
149         const newProperties = properties.filter(({ field, group }) => {
150             return !VCARD_KEY_FIELDS.includes(field) || (group && group !== emailGroup);
151         });
152         newProperties.push(...(await getKeysProperties(emailGroup || '', model)));
154         const mimeType = getMimeTypeVcard(model.mimeType);
155         if (mimeType) {
156             newProperties.push({
157                 field: 'x-pm-mimetype',
158                 value: mimeType,
159                 group: emailGroup,
160                 uid: createContactPropertyUid(),
161             });
162         }
164         if (model.isPGPExternal) {
165             const hasPinnedKeys = model.publicKeys.pinnedKeys.length > 0;
166             const hasApiKeys = model.publicKeys.apiKeys.length > 0; // from WKD or other untrusted servers
168             if ((hasPinnedKeys || hasApiKeys) && model.encrypt !== undefined) {
169                 newProperties.push({
170                     field: hasPinnedKeys ? 'x-pm-encrypt' : 'x-pm-encrypt-untrusted',
171                     value: `${model.encrypt}`,
172                     group: emailGroup,
173                     uid: createContactPropertyUid(),
174                 });
175             }
177             // Encryption automatically enables signing (but we do not store the info for non-pinned WKD keys).
178             const sign = model.encrypt || model.sign;
179             if (sign !== undefined) {
180                 newProperties.push({
181                     field: 'x-pm-sign',
182                     value: `${sign}`,
183                     group: emailGroup,
184                     uid: createContactPropertyUid(),
185                 });
186             }
188             if (model.scheme) {
189                 newProperties.push({
190                     field: 'x-pm-scheme',
191                     value: model.scheme,
192                     group: emailGroup,
193                     uid: createContactPropertyUid(),
194                 });
195             }
196         }
198         const newVCardContact = fromVCardProperties(newProperties);
200         try {
201             await saveVCardContact(contactID, newVCardContact);
203             await call();
205             createNotification({ text: c('Success').t`Preferences saved` });
206         } finally {
207             rest.onClose?.();
208         }
209     };
211     useEffect(() => {
212         /**
213          * On the first render, initialize the model
214          */
215         if (!model) {
216             void withLoadingPgpSettings(prepare());
217             return;
218         }
219         /**
220          * When the list of trusted, expired or revoked keys change,
221          * * update the list:
222          * * re-check if the new keys can send
223          * * re-order api keys (trusted take preference)
224          * * move expired keys to the bottom of the list
225          */
227         setModel((model?: ContactPublicKeyModelWithApiKeySource) => {
228             if (!model) {
229                 return;
230             }
231             const {
232                 publicKeys,
233                 trustedFingerprints,
234                 obsoleteFingerprints,
235                 compromisedFingerprints,
236                 encryptionCapableFingerprints,
237             } = model;
238             const apiKeys = sortApiKeys({
239                 keys: publicKeys.apiKeys,
240                 trustedFingerprints,
241                 obsoleteFingerprints,
242                 compromisedFingerprints,
243             });
244             const pinnedKeys = sortPinnedKeys({
245                 keys: publicKeys.pinnedKeys,
246                 obsoleteFingerprints,
247                 compromisedFingerprints,
248                 encryptionCapableFingerprints,
249             });
250             const verifyingPinnedKeys = getVerifyingKeys(pinnedKeys, model.compromisedFingerprints);
252             return {
253                 ...model,
254                 publicKeys: { apiKeys, pinnedKeys, verifyingPinnedKeys },
255             };
256         });
257     }, [
258         model?.trustedFingerprints,
259         model?.obsoleteFingerprints,
260         model?.encryptionCapableFingerprints,
261         model?.compromisedFingerprints,
262     ]);
264     useEffect(() => {
265         // take into account rules relating email format and cryptographic scheme
266         if (!isMimeTypeFixed) {
267             return;
268         }
269         // PGP/Inline should force the email format to plaintext
270         if (hasPGPInline) {
271             return setModel((model?: ContactPublicKeyModelWithApiKeySource) => {
272                 if (!model) {
273                     return;
274                 }
275                 return { ...model, mimeType: MIME_TYPES.PLAINTEXT };
276             });
277         }
278         // If PGP/Inline is not selected, go back to automatic
279         setModel((model?: ContactPublicKeyModelWithApiKeySource) => {
280             if (!model) {
281                 return;
282             }
283             return { ...model, mimeType: MIME_TYPES_MORE.AUTOMATIC };
284         });
285     }, [isMimeTypeFixed, hasPGPInline]);
287     return (
288         <ModalTwo size="large" className="contacts-modal" {...rest}>
289             <ModalTwoHeader
290                 title={c('Title').t`Edit email settings`}
291                 titleClassName="text-ellipsis"
292                 subline={emailAddress}
293             />
294             <ModalTwoContent>
295                 {!isMimeTypeFixed ? (
296                     <Alert className="mb-4">
297                         {c('Info')
298                             .t`Select the email format you want to be used by default when sending an email to this email address.`}
299                     </Alert>
300                 ) : null}
301                 {isMimeTypeFixed && hasPGPInline ? (
302                     <Alert className="mb-4">{c('Info').t`PGP/Inline is only compatible with Plain Text format.`}</Alert>
303                 ) : null}
304                 {isMimeTypeFixed && !hasPGPInline ? (
305                     <Alert className="mb-4">
306                         {c('Info').t`PGP/MIME automatically sends the message using the current composer mode.`}
307                     </Alert>
308                 ) : null}
309                 <Row>
310                     <Label>
311                         {c('Label').t`Email format`}
312                         <Info
313                             className="ml-2"
314                             title={c('Tooltip')
315                                 .t`Automatic indicates that the format in the composer is used to send to this user. Plain text indicates that the message will always be converted to plain text on send.`}
316                         />
317                     </Label>
318                     <Field>
319                         <ContactMIMETypeSelect
320                             disabled={loadingSave || isMimeTypeFixed}
321                             value={model?.mimeType || ''}
322                             onChange={(mimeType: CONTACT_MIME_TYPES) =>
323                                 setModel((model?: ContactPublicKeyModelWithApiKeySource) => {
324                                     if (!model) {
325                                         return;
326                                     }
327                                     return { ...model, mimeType };
328                                 })
329                             }
330                         />
331                     </Field>
332                 </Row>
333                 <div className="mb-4">
334                     <Collapsible disabled={loadingPgpSettings}>
335                         <CollapsibleHeader
336                             suffix={
337                                 <CollapsibleHeaderIconButton onClick={() => setShowPgpSettings(!showPgpSettings)}>
338                                     <Icon name="chevron-down" />
339                                 </CollapsibleHeaderIconButton>
340                             }
341                             disableFullWidth
342                             onClick={() => setShowPgpSettings(!showPgpSettings)}
343                             className={clsx([
344                                 'color-primary',
345                                 loadingPgpSettings ? 'color-weak text-no-decoration' : 'text-underline',
346                             ])}
347                         >
348                             {showPgpSettings
349                                 ? c('Action').t`Hide advanced PGP settings`
350                                 : c('Action').t`Show advanced PGP settings`}
351                         </CollapsibleHeader>
352                         <CollapsibleContent className="mt-4">
353                             {showPgpSettings && model ? (
354                                 <ContactPGPSettings model={model} setModel={setModel} mailSettings={mailSettings} />
355                             ) : null}
356                         </CollapsibleContent>
357                     </Collapsible>
358                 </div>
359             </ModalTwoContent>
360             <ModalTwoFooter>
361                 <Button type="reset" onClick={rest.onClose}>{c('Action').t`Cancel`}</Button>
362                 <Button
363                     color="norm"
364                     loading={loadingSave}
365                     disabled={loadingSave || loadingPgpSettings}
366                     type="submit"
367                     onClick={() => withLoadingSave(handleSubmit(model))}
368                     data-testid="email-settings:save"
369                 >
370                     {c('Action').t`Save`}
371                 </Button>
372             </ModalTwoFooter>
373         </ModalTwo>
374     );
377 export default ContactEmailSettingsModal;