Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / components / containers / members / UserInviteOrEditModal.tsx
blob2fa9772e703be384c3fc378f92c815a894c752b6
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 = ({
41     organization,
42     member,
43     allowStorageConfiguration,
44     allowAIAssistantConfiguration,
45     aiSeatsRemaining,
46     ...modalState
47 }: Props) => {
48     const api = useApi();
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(
64         () => ({
65             address: '',
66             storage: member
67                 ? member.MaxSpace
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,
72         }),
73         [member]
74     );
75     const [model, setModel] = useState(initialModel);
77     const handleClose = () => {
78         if (submitting) {
79             return;
80         }
82         modalState.onClose();
83     };
85     const handleChange = (key: keyof typeof model) => (value: (typeof model)[typeof key]) => {
86         setModel({ ...model, [key]: value });
87     };
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));
95         }
97         createNotification({ text: c('Success').t`Invitation sent` });
98     };
100     const editInvitation = async () => {
101         let updated = false;
102         await api(editMemberInvitation(member!.ID, model.storage));
104         if (allowAIAssistantConfiguration) {
105             await api(updateAI(member!.ID, model.numAI ? 1 : 0));
106         }
108         updated = true;
109         if (updated) {
110             createNotification({ text: c('familyOffer_2023:Success').t`Member updated` });
111         }
112     };
114     const handleSubmit = async () => {
115         if (isEditing) {
116             await editInvitation();
117         } else {
118             await sendInvitation();
119         }
120         await call();
121         modalState.onClose();
122     };
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;
142     return (
143         <Modal
144             as="form"
145             size="large"
146             {...modalState}
147             onClose={handleClose}
148             noValidate
149             onSubmit={(event: FormEvent) => {
150                 event.preventDefault();
151                 event.stopPropagation();
152                 if (!onFormSubmit()) {
153                     return;
154                 }
155                 void withLoading(handleSubmit());
156             }}
157         >
158             <ModalHeader title={modalTitle} />
159             <ModalContent>
160                 <p className="color-weak">{modalDescription}</p>
162                 {!isEditing && (
163                     <InputFieldTwo
164                         id="email-address"
165                         type="email"
166                         autoCapitalize="off"
167                         autoComplete="off"
168                         autoCorrect="off"
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}
175                         autoFocus
176                     />
177                 )}
179                 {allowAIAssistantConfiguration && (
180                     <div className="mb-4">
181                         <MemberToggleContainer
182                             toggle={
183                                 <Toggle
184                                     id="ai-assistant-toggle"
185                                     checked={model.numAI}
186                                     disabled={!aiSeatsRemaining}
187                                     onChange={({ target }) => handleChange('numAI')(target.checked)}
188                                 />
189                             }
190                             label={
191                                 <label className="text-semibold" htmlFor="ai-assistant-toggle">
192                                     {c('Info').t`Writing assistant`}
193                                 </label>
194                             }
195                             assistiveText={
196                                 !aiSeatsRemaining && !model.numAI ? <AssistantUpdateSubscriptionButton /> : undefined
197                             }
198                         />
199                     </div>
200                 )}
202                 {allowStorageConfiguration && (
203                     <MemberStorageSelector
204                         value={model.storage}
205                         disabled={submitting}
206                         sizeUnit={storageSizeUnit}
207                         range={storageRange}
208                         totalStorage={totalStorage}
209                         onChange={handleChange('storage')}
210                     />
211                 )}
212             </ModalContent>
213             <ModalFooter>
214                 <Button onClick={handleClose} disabled={submitting}>
215                     {c('Action').t`Cancel`}
216                 </Button>
217                 <Button loading={submitting} type="submit" color="norm">
218                     {isEditing ? c('Action').t`Save` : c('Action').t`Invite`}
219                 </Button>
220             </ModalFooter>
221         </Modal>
222     );
225 export default UserInviteOrEditModal;