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';
36 confirmPasswordValidator,
38 passwordLengthValidator,
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';
44 getCanGenerateMemberKeys,
45 getShouldSetupMemberKeys,
46 missingKeysMemberProcess,
47 missingKeysSelfProcess,
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'> {
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('');
71 const initialMember = member || members[0];
72 const authentication = useAuthentication();
73 const [model, setModel] = useState(() => {
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,
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 };
112 const [Local, Domain] = getEmailParts(address);
114 return { Local, Domain };
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');
124 const organizationKey = await getOrganizationKey();
125 const DisplayName = model.name;
127 if (!hasPremium && `${user.Name}@${premiumDomain}`.toLowerCase() === emailAddress.toLowerCase()) {
128 return createNotification({
130 .t`${user.Name} is your username. To create ${emailAddress}, please go to Settings > Messages and composing > Short domain (pm.me)`,
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' });
143 const { Address } = await api<{ Address: Address }>(
145 MemberID: selectedMember.ID,
146 Local: emailAddressParts.Local,
147 Domain: emailAddressParts.Domain,
152 if (shouldGenerateKeys) {
153 const userKeys = await getUserKeys();
155 if (shouldGenerateSelfKeys) {
156 await missingKeysSelfProcess({
160 addressesToGenerate: [Address],
161 password: authentication.getPassword(),
164 keyTransparencyVerify,
167 if (!organizationKey?.privateKey) {
168 throw new Error('Missing org key');
170 const memberAddresses = await getAllMemberAddresses(api, selectedMember.ID);
171 if (shouldSetupMemberKeys && password) {
172 await setupMemberKeys({
173 ownerAddresses: addresses,
175 organizationKey: organizationKey.privateKey,
176 member: selectedMember,
180 keyTransparencyVerify,
183 await missingKeysMemberProcess({
186 ownerAddresses: addresses,
187 memberAddressesToGenerate: [Address],
188 member: selectedMember,
191 organizationKey: organizationKey.privateKey,
192 keyTransparencyVerify,
197 await keyTransparencyCommit(userKeys);
203 createNotification({ text: c('Success').t`Address added` });
206 const handleClose = submitting ? undefined : rest.onClose;
208 const getMemberName = (member: Member | undefined) => {
209 return (member?.Self && user.Name ? user.Name : member?.Name) || '';
216 onSubmit={(event: FormEvent) => {
217 event.preventDefault();
218 event.stopPropagation();
219 if (!onFormSubmit()) {
222 withLoading(handleSubmit());
224 onClose={handleClose}
226 <ModalHeader title={c('Title').t`Add address`} />
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>
234 aria-describedby="label-user-select"
236 onChange={({ value }) => setModel({ ...model, id: value })}
238 {members.map((member) => {
239 const name = getMemberName(member);
246 member.State === MEMBER_STATE.STATUS_DISABLED ||
247 member.State === MEMBER_STATE.STATUS_INVITED
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"
272 if (loadingDomains) {
273 return <CircleLoader />;
275 if (domainOptions.length === 0) {
278 if (domainOptions.length === 1) {
281 className="text-ellipsis"
282 id="user-domain-selected"
283 title={`@${domainOptions[0].value}`}
285 @{domainOptions[0].value}
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"
299 {domainOptions.map((option) => (
300 <Option key={option.value} value={option.value} title={`@${option.text}`}>
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"
318 {shouldSetupMemberKeys && (
320 <div className="mb-4 color-weak">
322 .t`Before creating this address you need to provide a password and create encryption keys for it.`}
327 as={PasswordInputTwo}
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"
337 as={PasswordInputTwo}
338 label={c('Label').t`Confirm password`}
339 placeholder={c('Placeholder').t`Confirm`}
340 value={confirmPassword}
341 onValue={setConfirmPassword}
343 passwordLengthValidator(confirmPassword),
344 confirmPasswordValidator(confirmPassword, password),
346 autoComplete="new-password"
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>
359 export default AddressModal;