1 import { call, put, select, takeEvery } from 'redux-saga/effects';
2 import { c } from 'ttag';
4 import { PassErrorCode } from '@proton/pass/lib/api/errors';
5 import { getPrimaryPublicKeyForEmail } from '@proton/pass/lib/auth/address';
6 import { createNewUserInvites, createUserInvites } from '@proton/pass/lib/invites/invite.requests';
7 import { moveItem } from '@proton/pass/lib/items/item.requests';
8 import { createVault } from '@proton/pass/lib/vaults/vault.requests';
10 inviteBatchCreateFailure,
11 inviteBatchCreateIntent,
12 inviteBatchCreateSuccess,
14 } from '@proton/pass/store/actions';
15 import { selectItem, selectPassPlan, selectVaultSharedWithEmails } from '@proton/pass/store/selectors';
16 import type { RootSagaOptions } from '@proton/pass/store/types';
17 import type { ItemMoveDTO, ItemRevision, Maybe, Share, ShareType } from '@proton/pass/types';
18 import { UserPassPlan } from '@proton/pass/types/api/plan';
19 import type { InviteMemberDTO, InviteUserDTO } from '@proton/pass/types/data/invites.dto';
20 import { partition } from '@proton/pass/utils/array/partition';
21 import { getApiError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
23 function* createInviteWorker(
24 { onNotification }: RootSagaOptions,
25 { payload, meta: { request } }: ReturnType<typeof inviteBatchCreateIntent>
27 const count = payload.members.length;
28 const plan: UserPassPlan = yield select(selectPassPlan);
31 const shareId: string = !payload.withVaultCreation
33 : yield call(function* () {
34 /** create a new vault and move the item provided in the
35 * action's payload if the user is creating an invite through
36 * the `move to a new shared vault` flow */
37 const { name, description, icon, color, item } = payload;
38 const vaultContent = { name, description, display: { icon, color } };
39 const share: Share<ShareType.Vault> = yield createVault({ content: vaultContent });
41 const itemToMove: Maybe<ItemRevision> = item
42 ? yield select(selectItem(item.shareId, item.itemId))
45 const move: Maybe<ItemMoveDTO> =
47 ? { before: itemToMove, after: yield moveItem(itemToMove, item.shareId, share.shareId) }
50 yield put(sharedVaultCreated({ share, move }));
54 /** Filter out members that may be already invited or members of the vault */
55 const vaultSharedWith: Set<string> = yield select(selectVaultSharedWithEmails(shareId));
56 const members = payload.members.filter(({ value }) => !vaultSharedWith.has(value.email));
58 /** resolve each member's public key: if the member is not a
59 * proton user, `publicKey` will be `undefined` and we should
60 * treat it as as a new user invite */
61 const membersDTO: InviteMemberDTO[] = yield Promise.all(
62 members.map<Promise<InviteMemberDTO>>(async ({ value: { email, role } }) => ({
64 publicKey: await getPrimaryPublicKeyForEmail(email),
69 const [users, newUsers] = partition(
70 membersDTO /** split existing users from new users */,
71 (dto): dto is InviteUserDTO => 'publicKey' in dto && dto.publicKey !== undefined
74 /** Both `createUserInvites` & `createNewUserInvite` return the
75 * list of emails which could not be sent out. On success, both
77 const failedUsers: string[] = yield createUserInvites(shareId, users);
78 const failedNewUsers: string[] = yield createNewUserInvites(shareId, newUsers);
79 const failed = failedUsers.concat(failedNewUsers);
81 const totalFailure = failed.length === members.length;
82 const hasFailures = failedUsers.length > 0 || failedNewUsers.length > 0;
84 if (totalFailure) throw new Error('Could not send invitations');
89 // Translator: list of failed invited emails is appended
90 text: c('Warning').t`Could not send invitations to the following addresses:` + ` ${failed.join(', ')}`,
94 yield put(inviteBatchCreateSuccess(request.id, { shareId }, members.length - failed.length));
95 } catch (error: unknown) {
96 /** Fine-tune the error message when a B2B user
97 * reaches the 100 members per vault hard-limit */
98 if (plan === UserPassPlan.BUSINESS && error instanceof Error && 'data' in error) {
99 const apiError = error as any;
100 const { code } = getApiError(apiError);
102 if (code === PassErrorCode.RESOURCE_LIMIT_EXCEEDED) {
103 apiError.data.Error = c('Warning').t`Please contact us to investigate the issue`;
107 yield put(inviteBatchCreateFailure(request.id, error, count));
111 export default function* watcher(options: RootSagaOptions) {
112 yield takeEvery(inviteBatchCreateIntent.match, createInviteWorker, options);