1 import type { FormEvent } from 'react';
2 import { useMemo, useState } from 'react';
4 import { c } from 'ttag';
6 import { useSubscription } from '@proton/account/subscription/hooks';
7 import { Button } from '@proton/atoms';
8 import Modal from '@proton/components/components/modalTwo/Modal';
9 import ModalContent from '@proton/components/components/modalTwo/ModalContent';
10 import ModalFooter from '@proton/components/components/modalTwo/ModalFooter';
11 import ModalHeader from '@proton/components/components/modalTwo/ModalHeader';
12 import type { ModalStateProps } from '@proton/components/components/modalTwo/useModalState';
13 import Toggle from '@proton/components/components/toggle/Toggle';
14 import InputFieldTwo from '@proton/components/components/v2/field/InputField';
15 import useFormErrors from '@proton/components/components/v2/useFormErrors';
16 import AssistantUpdateSubscriptionButton from '@proton/components/containers/payments/subscription/assistant/AssistantUpdateSubscriptionButton';
17 import useApi from '@proton/components/hooks/useApi';
18 import useEventManager from '@proton/components/hooks/useEventManager';
19 import useNotifications from '@proton/components/hooks/useNotifications';
20 import { useLoading } from '@proton/hooks';
21 import { editMemberInvitation, inviteMember, updateAI } from '@proton/shared/lib/api/members';
22 import { BRAND_NAME, MAIL_APP_NAME, MEMBER_ROLE } from '@proton/shared/lib/constants';
23 import { emailValidator, requiredValidator } from '@proton/shared/lib/helpers/formValidators';
24 import { sizeUnits } from '@proton/shared/lib/helpers/size';
25 import { hasDuo, hasFamily, hasPassFamily, hasVisionary } from '@proton/shared/lib/helpers/subscription';
26 import type { Member, Organization } from '@proton/shared/lib/interfaces';
27 import clamp from '@proton/utils/clamp';
29 import MemberStorageSelector, { getInitialStorage, getStorageRange, getTotalStorage } from './MemberStorageSelector';
30 import MemberToggleContainer from './MemberToggleContainer';
32 interface Props extends ModalStateProps {
33 organization?: Organization;
34 member: Member | null | undefined;
35 allowAIAssistantConfiguration: boolean;
36 aiSeatsRemaining: boolean;
37 allowStorageConfiguration?: boolean;
40 const UserInviteOrEditModal = ({
43 allowStorageConfiguration,
44 allowAIAssistantConfiguration,
49 const { call } = useEventManager();
50 const [submitting, withLoading] = useLoading();
51 const { createNotification } = useNotifications();
52 const { validator, onFormSubmit } = useFormErrors();
53 const totalStorage = getTotalStorage(member ?? {}, organization);
54 const storageRange = getStorageRange(member ?? {}, organization);
55 const storageSizeUnit = sizeUnits.GB;
56 const isEditing = !!member?.ID;
58 const [subscription] = useSubscription();
59 const isVisionary = hasVisionary(subscription);
60 const isDuo = hasDuo(subscription);
61 const isFamily = hasFamily(subscription);
63 const initialModel = useMemo(
68 : clamp(getInitialStorage(organization, storageRange), storageRange.min, storageRange.max),
69 vpn: !!member?.MaxVPN,
70 numAI: aiSeatsRemaining && (isVisionary || isDuo || isFamily), // Visionary, Duo and Family users should have the toggle set to true by default
71 admin: member?.Role === MEMBER_ROLE.ORGANIZATION_ADMIN,
75 const [model, setModel] = useState(initialModel);
77 const handleClose = () => {
85 const handleChange = (key: keyof typeof model) => (value: (typeof model)[typeof key]) => {
86 setModel({ ...model, [key]: value });
89 const sendInvitation = async () => {
90 const res = await api(inviteMember(model.address, model.storage));
92 // Users could have the Writing Assistant enabled when created and we need to update the member when this is the case
93 if (allowAIAssistantConfiguration) {
94 await api(updateAI(res.Member.ID, model.numAI ? 1 : 0));
97 createNotification({ text: c('Success').t`Invitation sent` });
100 const editInvitation = async () => {
102 await api(editMemberInvitation(member!.ID, model.storage));
104 if (allowAIAssistantConfiguration) {
105 await api(updateAI(member!.ID, model.numAI ? 1 : 0));
110 createNotification({ text: c('familyOffer_2023:Success').t`Member updated` });
114 const handleSubmit = async () => {
116 await editInvitation();
118 await sendInvitation();
121 modalState.onClose();
124 const editingCreateAccountCopyFamily = c('familyOffer_2023:Info')
125 .t`If the user already has a ${MAIL_APP_NAME} address, enter it here. Otherwise they need to create an account first.`;
126 const editingCreateAccountCopyFamilyWithAccount = c('familyOffer_2023:Info')
127 .t`If the user already has an account with ${BRAND_NAME}, enter it here. Otherwise they need to create an account first.`;
129 const editingCreateAccountCopy =
130 hasPassFamily(subscription) || hasVisionary(subscription)
131 ? editingCreateAccountCopyFamilyWithAccount
132 : editingCreateAccountCopyFamily;
134 const mailFieldValidator = !isEditing ? [requiredValidator(model.address), emailValidator(model.address)] : [];
135 const modalTitle = isEditing
136 ? c('familyOffer_2023:Title').t`Edit user storage`
137 : c('familyOffer_2023:Title').t`Invite a user`;
138 const modalDescription = isEditing
139 ? c('familyOffer_2023:Info').t`You can increase or reduce the storage for this user.`
140 : editingCreateAccountCopy;
147 onClose={handleClose}
149 onSubmit={(event: FormEvent) => {
150 event.preventDefault();
151 event.stopPropagation();
152 if (!onFormSubmit()) {
155 void withLoading(handleSubmit());
158 <ModalHeader title={modalTitle} />
160 <p className="color-weak">{modalDescription}</p>
169 value={model.address}
170 error={validator(mailFieldValidator)}
171 onValue={handleChange('address')}
172 label={c('Label').t`Email address`}
173 placeholder="thomas.anderson@proton.me"
174 disableChange={submitting}
179 {allowAIAssistantConfiguration && (
180 <div className="mb-4">
181 <MemberToggleContainer
184 id="ai-assistant-toggle"
185 checked={model.numAI}
186 disabled={!aiSeatsRemaining}
187 onChange={({ target }) => handleChange('numAI')(target.checked)}
191 <label className="text-semibold" htmlFor="ai-assistant-toggle">
192 {c('Info').t`Writing assistant`}
196 !aiSeatsRemaining && !model.numAI ? <AssistantUpdateSubscriptionButton /> : undefined
202 {allowStorageConfiguration && (
203 <MemberStorageSelector
204 value={model.storage}
205 disabled={submitting}
206 sizeUnit={storageSizeUnit}
208 totalStorage={totalStorage}
209 onChange={handleChange('storage')}
214 <Button onClick={handleClose} disabled={submitting}>
215 {c('Action').t`Cancel`}
217 <Button loading={submitting} type="submit" color="norm">
218 {isEditing ? c('Action').t`Save` : c('Action').t`Invite`}
225 export default UserInviteOrEditModal;