Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / pass / store / sagas / invites / invite-create.saga.ts
blobe9cb28604149fcbcbc67a9055aefc469d79e5025
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';
9 import {
10     inviteBatchCreateFailure,
11     inviteBatchCreateIntent,
12     inviteBatchCreateSuccess,
13     sharedVaultCreated,
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>
26 ) {
27     const count = payload.members.length;
28     const plan: UserPassPlan = yield select(selectPassPlan);
30     try {
31         const shareId: string = !payload.withVaultCreation
32             ? payload.shareId
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))
43                       : undefined;
45                   const move: Maybe<ItemMoveDTO> =
46                       itemToMove && item
47                           ? { before: itemToMove, after: yield moveItem(itemToMove, item.shareId, share.shareId) }
48                           : undefined;
50                   yield put(sharedVaultCreated({ share, move }));
51                   return share.shareId;
52               });
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 } }) => ({
63                 email: email,
64                 publicKey: await getPrimaryPublicKeyForEmail(email),
65                 role,
66             }))
67         );
69         const [users, newUsers] = partition(
70             membersDTO /** split existing users from new users  */,
71             (dto): dto is InviteUserDTO => 'publicKey' in dto && dto.publicKey !== undefined
72         );
74         /** Both `createUserInvites` & `createNewUserInvite` return the
75          * list of emails which could not be sent out. On success, both
76          * should be empty */
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');
86         if (hasFailures) {
87             onNotification?.({
88                 type: 'error',
89                 // Translator: list of failed invited emails is appended
90                 text: c('Warning').t`Could not send invitations to the following addresses:` + ` ${failed.join(', ')}`,
91             });
92         }
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`;
104             }
105         }
107         yield put(inviteBatchCreateFailure(request.id, error, count));
108     }
111 export default function* watcher(options: RootSagaOptions) {
112     yield takeEvery(inviteBatchCreateIntent.match, createInviteWorker, options);