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 };
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')),
51 const invites = useMemo<InviteListItem[]>(
54 ...(vault.invites ?? []).map((invite) => ({
55 key: invite.invitedEmail,
56 type: 'existing' as const,
59 ...(vault.newUserInvites ?? []).map((invite) => ({
60 key: invite.invitedEmail,
64 ].sort(sortOn('key', 'ASC')),
68 const memberLimitReached = isVaultMemberLimitReached(vault);
70 const warning = (() => {
71 if (canManage && memberLimitReached) {
75 label={c('Action').t`Upgrade now to share with more people`}
76 upsellRef={UpsellRef.LIMIT_SHARING}
77 key="access-upgrade-link"
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.`;
87 <SidebarModal onClose={close} open>
94 key="modal-close-button"
101 <Icon className="modal-close-icon" name="cross-big" alt={c('Action').t`Close`} />
105 key="modal-invite-button"
108 onClick={() => (memberLimitReached ? setLimitModalOpen(true) : createInvite({ vault }))}
109 disabled={!canManage || (plan === UserPassPlan.FREE && memberLimitReached)}
111 {c('Action').t`Invite others`}
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}
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) => {
134 shareId={vault.shareId}
136 email={item.invite.invitedEmail}
137 newUserInviteId={item.invite.newUserInviteId}
138 canManage={canManage}
139 state={item.invite.state}
144 <PendingExistingMember
146 shareId={vault.shareId}
147 email={item.invite.invitedEmail}
148 inviteId={item.invite.inviteId}
149 canManage={canManage}
155 {members.length > 0 && <span className="color-weak">{c('Label').t`Members`}</span>}
157 {members.map((member) => (
161 shareId={vault.shareId}
162 userShareId={member.shareId}
163 me={vault.shareId === member.shareId}
165 role={member.shareRoleId}
166 canManage={canManage}
167 canTransfer={vault.owner && hasMultipleOwnedWritableVaults}
171 <Card type="primary" className="text-sm">
177 <div className="absolute inset-center flex flex-column gap-y-3 text-center color-weak text-sm">
179 .t`This vault is not currently shared with anyone. Invite people to share it with others.`}
187 onClick={() => setLimitModalOpen(false)}
195 className="text-left"
196 onClose={() => setLimitModalOpen(false)}
197 open={limitModalOpen}
198 title={c('Title').t`Member limit`}
199 enableCloseWhenClickOutside
201 <Alert className="mb-4 text-sm" type="error">
204 {c('Error').t`Cannot send invitations at the moment`}{' '}
205 {c('Warning').t`Please contact us to investigate the issue`}
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