Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / addresses / AddressModal.tsx
blobebea4e78788c538bc1839801a7bb47275339001d
1 import type { FormEvent } from 'react';
2 import { useState } from 'react';
4 import { c } from 'ttag';
6 import { useAddresses } from '@proton/account/addresses/hooks';
7 import { useCustomDomains } from '@proton/account/domains/hooks';
8 import { useGetOrganizationKey } from '@proton/account/organizationKey/hooks';
9 import { useProtonDomains } from '@proton/account/protonDomains/hooks';
10 import { useUser } from '@proton/account/user/hooks';
11 import { useGetUserKeys } from '@proton/account/userKeys/hooks';
12 import { Button, CircleLoader } from '@proton/atoms';
13 import { DropdownSizeUnit } from '@proton/components/components/dropdown/utils';
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 PasswordInputTwo from '@proton/components/components/v2/input/PasswordInput';
23 import useFormErrors from '@proton/components/components/v2/useFormErrors';
24 import useKTVerifier from '@proton/components/containers/keyTransparency/useKTVerifier';
25 import useApi from '@proton/components/hooks/useApi';
26 import useAuthentication from '@proton/components/hooks/useAuthentication';
27 import useEventManager from '@proton/components/hooks/useEventManager';
28 import useNotifications from '@proton/components/hooks/useNotifications';
29 import { useLoading } from '@proton/hooks';
30 import { createAddress } from '@proton/shared/lib/api/addresses';
31 import { getAllMemberAddresses } from '@proton/shared/lib/api/members';
32 import { ADDRESS_TYPE, DEFAULT_KEYGEN_TYPE, KEYGEN_CONFIGS, MEMBER_PRIVATE } from '@proton/shared/lib/constants';
33 import { getAvailableAddressDomains } from '@proton/shared/lib/helpers/address';
34 import { getEmailParts } from '@proton/shared/lib/helpers/email';
35 import {
36     confirmPasswordValidator,
37     emailValidator,
38     passwordLengthValidator,
39     requiredValidator,
40 } from '@proton/shared/lib/helpers/formValidators';
41 import type { Address, Member } from '@proton/shared/lib/interfaces';
42 import { MEMBER_STATE } from '@proton/shared/lib/interfaces';
43 import {
44     getCanGenerateMemberKeys,
45     getShouldSetupMemberKeys,
46     missingKeysMemberProcess,
47     missingKeysSelfProcess,
48     setupMemberKeys,
49 } from '@proton/shared/lib/keys';
50 import noop from '@proton/utils/noop';
52 const keyGenConfig = KEYGEN_CONFIGS[DEFAULT_KEYGEN_TYPE];
54 interface Props extends ModalProps<'form'> {
55     member?: Member;
56     members: Member[];
57     useEmail?: boolean;
60 const AddressModal = ({ member, members, useEmail, ...rest }: Props) => {
61     const { call } = useEventManager();
62     const [user] = useUser();
63     const [addresses] = useAddresses();
64     const [customDomains, loadingCustomDomains] = useCustomDomains();
65     const [{ premiumDomains, protonDomains }, loadingProtonDomains] = useProtonDomains();
66     const loadingDomains = loadingCustomDomains || loadingProtonDomains;
67     const [premiumDomain = ''] = premiumDomains;
68     const [password, setPassword] = useState('');
69     const [confirmPassword, setConfirmPassword] = useState('');
70     const api = useApi();
71     const initialMember = member || members[0];
72     const authentication = useAuthentication();
73     const [model, setModel] = useState(() => {
74         return {
75             id: initialMember.ID,
76             name: '',
77             address: '',
78             domain: '',
79         };
80     });
81     const { createNotification } = useNotifications();
82     const { validator, onFormSubmit } = useFormErrors();
83     const [submitting, withLoading] = useLoading();
84     const hasPremium = addresses?.some(({ Type }) => Type === ADDRESS_TYPE.TYPE_PREMIUM);
85     const getOrganizationKey = useGetOrganizationKey();
87     const selectedMember = members.find((otherMember) => otherMember.ID === model.id);
88     const addressDomains = getAvailableAddressDomains({
89         member: selectedMember || initialMember,
90         user,
91         protonDomains,
92         premiumDomains,
93         customDomains,
94     });
95     const domainOptions = addressDomains.map((DomainName) => ({ text: DomainName, value: DomainName }));
96     const selectedDomain = model.domain || domainOptions[0]?.text;
97     const getUserKeys = useGetUserKeys();
98     const { keyTransparencyVerify, keyTransparencyCommit } = useKTVerifier(api, async () => user);
100     const shouldGenerateKeys =
101         !selectedMember || Boolean(selectedMember.Self) || getCanGenerateMemberKeys(selectedMember);
103     const shouldSetupMemberKeys = shouldGenerateKeys && getShouldSetupMemberKeys(selectedMember);
105     const getNormalizedAddress = () => {
106         const address = model.address.trim();
108         if (selectedDomain && !useEmail) {
109             return { Local: address, Domain: selectedDomain };
110         }
112         const [Local, Domain] = getEmailParts(address);
114         return { Local, Domain };
115     };
117     const emailAddressParts = getNormalizedAddress();
118     const emailAddress = `${emailAddressParts.Local}@${emailAddressParts.Domain}`;
120     const handleSubmit = async () => {
121         if (!selectedMember || !addresses) {
122             throw new Error('Missing member');
123         }
124         const organizationKey = await getOrganizationKey();
125         const DisplayName = model.name;
127         if (!hasPremium && `${user.Name}@${premiumDomain}`.toLowerCase() === emailAddress.toLowerCase()) {
128             return createNotification({
129                 text: c('Error')
130                     .t`${user.Name} is your username. To create ${emailAddress}, please go to Settings > Messages and composing > Short domain (pm.me)`,
131                 type: 'error',
132             });
133         }
135         const shouldGenerateSelfKeys =
136             Boolean(selectedMember.Self) && selectedMember.Private === MEMBER_PRIVATE.UNREADABLE;
137         const shouldGenerateMemberKeys = !shouldGenerateSelfKeys;
138         if (shouldGenerateKeys && shouldGenerateMemberKeys && !organizationKey?.privateKey) {
139             createNotification({ text: c('Error').t`Organization key is not decrypted`, type: 'error' });
140             return;
141         }
143         const { Address } = await api<{ Address: Address }>(
144             createAddress({
145                 MemberID: selectedMember.ID,
146                 Local: emailAddressParts.Local,
147                 Domain: emailAddressParts.Domain,
148                 DisplayName,
149             })
150         );
152         if (shouldGenerateKeys) {
153             const userKeys = await getUserKeys();
155             if (shouldGenerateSelfKeys) {
156                 await missingKeysSelfProcess({
157                     api,
158                     userKeys,
159                     addresses,
160                     addressesToGenerate: [Address],
161                     password: authentication.getPassword(),
162                     keyGenConfig,
163                     onUpdate: noop,
164                     keyTransparencyVerify,
165                 });
166             } else {
167                 if (!organizationKey?.privateKey) {
168                     throw new Error('Missing org key');
169                 }
170                 const memberAddresses = await getAllMemberAddresses(api, selectedMember.ID);
171                 if (shouldSetupMemberKeys && password) {
172                     await setupMemberKeys({
173                         ownerAddresses: addresses,
174                         keyGenConfig,
175                         organizationKey: organizationKey.privateKey,
176                         member: selectedMember,
177                         memberAddresses,
178                         password,
179                         api,
180                         keyTransparencyVerify,
181                     });
182                 } else {
183                     await missingKeysMemberProcess({
184                         api,
185                         keyGenConfig,
186                         ownerAddresses: addresses,
187                         memberAddressesToGenerate: [Address],
188                         member: selectedMember,
189                         memberAddresses,
190                         onUpdate: noop,
191                         organizationKey: organizationKey.privateKey,
192                         keyTransparencyVerify,
193                     });
194                 }
195             }
197             await keyTransparencyCommit(userKeys);
198         }
200         await call();
202         rest.onClose?.();
203         createNotification({ text: c('Success').t`Address added` });
204     };
206     const handleClose = submitting ? undefined : rest.onClose;
208     const getMemberName = (member: Member | undefined) => {
209         return (member?.Self && user.Name ? user.Name : member?.Name) || '';
210     };
212     return (
213         <Modal
214             as="form"
215             {...rest}
216             onSubmit={(event: FormEvent) => {
217                 event.preventDefault();
218                 event.stopPropagation();
219                 if (!onFormSubmit()) {
220                     return;
221                 }
222                 withLoading(handleSubmit());
223             }}
224             onClose={handleClose}
225         >
226             <ModalHeader title={c('Title').t`Add address`} />
227             <ModalContent>
228                 <div className="mb-6">
229                     <div className="text-semibold mb-1" id="label-user-select">{c('Label').t`User`}</div>
230                     {member || members?.length === 1 ? (
231                         <div className="text-ellipsis">{getMemberName(selectedMember)}</div>
232                     ) : (
233                         <SelectTwo
234                             aria-describedby="label-user-select"
235                             value={model.id}
236                             onChange={({ value }) => setModel({ ...model, id: value })}
237                         >
238                             {members.map((member) => {
239                                 const name = getMemberName(member);
240                                 return (
241                                     <Option
242                                         key={member.ID}
243                                         value={member.ID}
244                                         title={name}
245                                         disabled={
246                                             member.State === MEMBER_STATE.STATUS_DISABLED ||
247                                             member.State === MEMBER_STATE.STATUS_INVITED
248                                         }
249                                     >
250                                         {name}
251                                     </Option>
252                                 );
253                             })}
254                         </SelectTwo>
255                     )}
256                 </div>
258                 <InputFieldTwo
259                     id="address"
260                     autoFocus
261                     value={model.address}
262                     error={validator([requiredValidator(model.address), emailValidator(emailAddress)])}
263                     aria-describedby="user-domain-selected"
264                     onValue={(address: string) => setModel({ ...model, address })}
265                     label={useEmail ? c('Label').t`Email` : c('Label').t`Address`}
266                     placeholder={c('Placeholder').t`Address`}
267                     data-testid="settings:identity-section:add-address:address"
268                     suffix={(() => {
269                         if (useEmail) {
270                             return null;
271                         }
272                         if (loadingDomains) {
273                             return <CircleLoader />;
274                         }
275                         if (domainOptions.length === 0) {
276                             return null;
277                         }
278                         if (domainOptions.length === 1) {
279                             return (
280                                 <span
281                                     className="text-ellipsis"
282                                     id="user-domain-selected"
283                                     title={`@${domainOptions[0].value}`}
284                                 >
285                                     @{domainOptions[0].value}
286                                 </span>
287                             );
288                         }
289                         return (
290                             <SelectTwo
291                                 unstyled
292                                 size={{ width: DropdownSizeUnit.Static }}
293                                 originalPlacement="bottom-end"
294                                 value={selectedDomain}
295                                 onChange={({ value }) => setModel({ ...model, domain: value })}
296                                 data-testid="settings:identity-section:add-address:domain-select"
297                                 id="user-domain-selected"
298                             >
299                                 {domainOptions.map((option) => (
300                                     <Option key={option.value} value={option.value} title={`@${option.text}`}>
301                                         @{option.text}
302                                     </Option>
303                                 ))}
304                             </SelectTwo>
305                         );
306                     })()}
307                 />
308                 {!useEmail && (
309                     <InputFieldTwo
310                         id="name"
311                         value={model.name}
312                         onValue={(name: string) => setModel({ ...model, name })}
313                         label={c('Label').t`Display name`}
314                         placeholder={c('Placeholder').t`Choose display name`}
315                         data-testid="settings:identity-section:add-address:display-name"
316                     />
317                 )}
318                 {shouldSetupMemberKeys && (
319                     <>
320                         <div className="mb-4 color-weak">
321                             {c('Info')
322                                 .t`Before creating this address you need to provide a password and create encryption keys for it.`}
323                         </div>
324                         <InputFieldTwo
325                             required
326                             id="password"
327                             as={PasswordInputTwo}
328                             value={password}
329                             error={validator([passwordLengthValidator(password), requiredValidator(password)])}
330                             onValue={setPassword}
331                             label={c('Label').t`Password`}
332                             placeholder={c('Placeholder').t`Password`}
333                             autoComplete="new-password"
334                         />
335                         <InputFieldTwo
336                             id="confirmPassword"
337                             as={PasswordInputTwo}
338                             label={c('Label').t`Confirm password`}
339                             placeholder={c('Placeholder').t`Confirm`}
340                             value={confirmPassword}
341                             onValue={setConfirmPassword}
342                             error={validator([
343                                 passwordLengthValidator(confirmPassword),
344                                 confirmPasswordValidator(confirmPassword, password),
345                             ])}
346                             autoComplete="new-password"
347                         />
348                     </>
349                 )}
350             </ModalContent>
351             <ModalFooter>
352                 <Button onClick={handleClose} disabled={submitting}>{c('Action').t`Cancel`}</Button>
353                 <Button color="norm" type="submit" loading={submitting}>{c('Action').t`Save address`}</Button>
354             </ModalFooter>
355         </Modal>
356     );
359 export default AddressModal;