Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / components / Invite / VaultAccessManager.tsx
blob1d3f07992a949fb40be3dbd1004dda7746a9791d
1 import { type FC, useMemo, useState } from 'react';
2 import { useSelector } from 'react-redux';
4 import { c, msgid } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import { Alert, Icon, Prompt } from '@proton/components';
8 import { UpgradeButton } from '@proton/pass/components/Layout/Button/UpgradeButton';
9 import { Card } from '@proton/pass/components/Layout/Card/Card';
10 import { SidebarModal } from '@proton/pass/components/Layout/Modal/SidebarModal';
11 import { Panel } from '@proton/pass/components/Layout/Panel/Panel';
12 import { PanelHeader } from '@proton/pass/components/Layout/Panel/PanelHeader';
13 import { ShareMember } from '@proton/pass/components/Share/ShareMember';
14 import { PendingExistingMember, PendingNewMember } from '@proton/pass/components/Share/SharePendingMember';
15 import { SharedVaultItem } from '@proton/pass/components/Vault/SharedVaultItem';
16 import { UpsellRef } from '@proton/pass/constants';
17 import { useShareAccessOptionsPolling } from '@proton/pass/hooks/useShareAccessOptionsPolling';
18 import { isShareManageable } from '@proton/pass/lib/shares/share.predicates';
19 import { isVaultMemberLimitReached } from '@proton/pass/lib/vaults/vault.predicates';
20 import { selectOwnWritableVaults, selectPassPlan, selectShareOrThrow } from '@proton/pass/store/selectors';
21 import type { NewUserPendingInvite, PendingInvite, ShareType } from '@proton/pass/types';
22 import { type ShareMember as ShareMemberType } from '@proton/pass/types';
23 import { UserPassPlan } from '@proton/pass/types/api/plan';
24 import { sortOn } from '@proton/pass/utils/fp/sort';
26 import { useInviteContext } from './InviteContext';
28 type Props = { shareId: string };
30 type InviteListItem =
31     | { key: string; type: 'existing'; invite: PendingInvite }
32     | { key: string; type: 'new'; invite: NewUserPendingInvite };
34 export const VaultAccessManager: FC<Props> = ({ shareId }) => {
35     const { createInvite, close } = useInviteContext();
37     const vault = useSelector(selectShareOrThrow<ShareType.Vault>(shareId));
38     const plan = useSelector(selectPassPlan);
39     const hasMultipleOwnedWritableVaults = useSelector(selectOwnWritableVaults).length > 1;
41     const [limitModalOpen, setLimitModalOpen] = useState(false);
43     const loading = useShareAccessOptionsPolling(shareId);
44     const canManage = isShareManageable(vault);
45     const b2b = plan === UserPassPlan.BUSINESS;
47     const members = useMemo<ShareMemberType[]>(
48         () => (vault.members ?? []).slice().sort(sortOn('email', 'ASC')),
49         [vault]
50     );
51     const invites = useMemo<InviteListItem[]>(
52         () =>
53             [
54                 ...(vault.invites ?? []).map((invite) => ({
55                     key: invite.invitedEmail,
56                     type: 'existing' as const,
57                     invite,
58                 })),
59                 ...(vault.newUserInvites ?? []).map((invite) => ({
60                     key: invite.invitedEmail,
61                     type: 'new' as const,
62                     invite,
63                 })),
64             ].sort(sortOn('key', 'ASC')),
65         [vault]
66     );
68     const memberLimitReached = isVaultMemberLimitReached(vault);
70     const warning = (() => {
71         if (canManage && memberLimitReached) {
72             const upgradeLink = (
73                 <UpgradeButton
74                     inline
75                     label={c('Action').t`Upgrade now to share with more people`}
76                     upsellRef={UpsellRef.LIMIT_SHARING}
77                     key="access-upgrade-link"
78                 />
79             );
80             return plan === UserPassPlan.FREE
81                 ? c('Warning').jt`You have reached the limit of users in this vault. ${upgradeLink}`
82                 : c('Warning').t`You have reached the limit of members who can access this vault.`;
83         }
84     })();
86     return (
87         <SidebarModal onClose={close} open>
88             <Panel
89                 loading={loading}
90                 header={
91                     <PanelHeader
92                         actions={[
93                             <Button
94                                 key="modal-close-button"
95                                 className="shrink-0"
96                                 icon
97                                 pill
98                                 shape="solid"
99                                 onClick={close}
100                             >
101                                 <Icon className="modal-close-icon" name="cross-big" alt={c('Action').t`Close`} />
102                             </Button>,
104                             <Button
105                                 key="modal-invite-button"
106                                 color="norm"
107                                 pill
108                                 onClick={() => (memberLimitReached ? setLimitModalOpen(true) : createInvite({ vault }))}
109                                 disabled={!canManage || (plan === UserPassPlan.FREE && memberLimitReached)}
110                             >
111                                 {c('Action').t`Invite others`}
112                             </Button>,
113                         ]}
114                     />
115                 }
116             >
117                 <SharedVaultItem
118                     className="mt-3 mb-6"
119                     color={vault.content.display.color}
120                     icon={vault.content.display.icon}
121                     name={vault.content.name}
122                     shareId={vault.shareId}
123                 />
125                 {vault.shared ? (
126                     <div className="flex flex-column gap-y-3">
127                         {invites.length > 0 && <span className="color-weak">{c('Label').t`Invitations`}</span>}
129                         {invites.map((item) => {
130                             switch (item.type) {
131                                 case 'new':
132                                     return (
133                                         <PendingNewMember
134                                             shareId={vault.shareId}
135                                             key={item.key}
136                                             email={item.invite.invitedEmail}
137                                             newUserInviteId={item.invite.newUserInviteId}
138                                             canManage={canManage}
139                                             state={item.invite.state}
140                                         />
141                                     );
142                                 case 'existing':
143                                     return (
144                                         <PendingExistingMember
145                                             key={item.key}
146                                             shareId={vault.shareId}
147                                             email={item.invite.invitedEmail}
148                                             inviteId={item.invite.inviteId}
149                                             canManage={canManage}
150                                         />
151                                     );
152                             }
153                         })}
155                         {members.length > 0 && <span className="color-weak">{c('Label').t`Members`}</span>}
157                         {members.map((member) => (
158                             <ShareMember
159                                 key={member.email}
160                                 email={member.email}
161                                 shareId={vault.shareId}
162                                 userShareId={member.shareId}
163                                 me={vault.shareId === member.shareId}
164                                 owner={member.owner}
165                                 role={member.shareRoleId}
166                                 canManage={canManage}
167                                 canTransfer={vault.owner && hasMultipleOwnedWritableVaults}
168                             />
169                         ))}
170                         {warning && (
171                             <Card type="primary" className="text-sm">
172                                 {warning}
173                             </Card>
174                         )}
175                     </div>
176                 ) : (
177                     <div className="absolute inset-center flex flex-column gap-y-3 text-center color-weak text-sm">
178                         {c('Info')
179                             .t`This vault is not currently shared with anyone. Invite people to share it with others.`}
180                     </div>
181                 )}
183                 <Prompt
184                     buttons={
185                         <Button
186                             pill
187                             onClick={() => setLimitModalOpen(false)}
188                             className="w-full"
189                             shape="solid"
190                             color="weak"
191                         >
192                             {c('Action').t`OK`}
193                         </Button>
194                     }
195                     className="text-left"
196                     onClose={() => setLimitModalOpen(false)}
197                     open={limitModalOpen}
198                     title={c('Title').t`Member limit`}
199                     enableCloseWhenClickOutside
200                 >
201                     <Alert className="mb-4 text-sm" type="error">
202                         {b2b ? (
203                             <>
204                                 {c('Error').t`Cannot send invitations at the moment`}{' '}
205                                 {c('Warning').t`Please contact us to investigate the issue`}
206                             </>
207                         ) : (
208                             // translator: full message is "Vaults can’t contain more than 10 users.""
209                             c('Success').ngettext(
210                                 msgid`Vaults can’t contain more than ${vault.targetMaxMembers} user.`,
211                                 `Vaults can’t contain more than ${vault.targetMaxMembers} users.`,
212                                 vault.targetMaxMembers
213                             )
214                         )}
215                     </Alert>
216                 </Prompt>
217             </Panel>
218         </SidebarModal>
219     );