1 import type { UnknownAction } from '@reduxjs/toolkit';
2 import { createNextState, createSelector } from '@reduxjs/toolkit';
3 import type { ThunkAction } from 'redux-thunk';
4 import { c } from 'ttag';
6 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
7 import { getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
8 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
9 import { unprivatizeMemberKeysRoute } from '@proton/shared/lib/api/members';
10 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
14 MemberReadyForUnprivatization,
15 MemberReadyForUnprivatizationApproval,
16 VerifyOutboundPublicKeys,
17 } from '@proton/shared/lib/interfaces';
19 UnprivatizationRevisionError,
20 getMemberReadyForUnprivatization,
21 getMemberReadyForUnprivatizationApproval,
24 unprivatizeMemberHelper,
25 } from '@proton/shared/lib/keys';
27 import type { OrganizationKeyState } from '../organizationKey';
28 import { organizationKeyThunk } from '../organizationKey';
29 import { getMember } from './actions';
31 type UnprivatizationMemberApproval,
32 type UnprivatizationMemberFailure,
33 type UnprivatizationMemberSuccess,
36 import { type MembersState, selectMembers, setUnprivatizationState } from './index';
37 import { getMemberAddresses } from './index';
39 export const getMemberToUnprivatize = (member: Member): member is MemberReadyForUnprivatization => {
40 return getMemberReadyForUnprivatization(member.Unprivatization);
43 export const getMemberToUnprivatizeApproval = (member: Member): member is MemberReadyForUnprivatizationApproval => {
44 return getMemberReadyForUnprivatizationApproval(member.Unprivatization);
47 export const selectUnprivatizationState = (state: MembersState) => state.members.unprivatization;
50 | (UnprivatizationMemberApproval & {
51 member: MemberReadyForUnprivatizationApproval;
53 | (UnprivatizationMemberFailure & {
54 member: MemberReadyForUnprivatization;
56 | (UnprivatizationMemberSuccess & {
57 member: EnhancedMember;
60 const getJoinedUnprivatizationMemberList = (
61 unprivatizationState: ReturnType<typeof selectUnprivatizationState>,
62 membersState: ReturnType<typeof selectMembers>
63 ): JoinedResult[] => {
64 const membersList = Object.entries(unprivatizationState.members).map(([key, value]) => {
70 let membersMap: { [key: string]: EnhancedMember } = {};
71 if (membersList.length > 0 && membersState.value) {
72 membersMap = Object.fromEntries(membersState.value.map((member) => [member.ID, member]));
74 return membersList.reduce<JoinedResult[]>((acc, cur) => {
75 const member = membersMap[cur.id];
76 if (!cur.value || !member) {
79 if (cur.value.type === 'approval' && getMemberToUnprivatizeApproval(member)) {
85 if (cur.value.type === 'error' && getMemberToUnprivatize(member)) {
91 if (cur.value.type === 'success') {
101 const reportSentryError = (error: any) => {
102 const sentryError = getSentryError(error);
104 captureMessage('Unprivatization: Error unprivatizing member', {
106 extra: { error: sentryError },
111 const getErrorState = (error: any) => {
112 const apiErrorMessage = getApiErrorMessage(error);
113 const errorMessage = apiErrorMessage || error?.message || c('Error').t`Unknown error`;
117 revision: error instanceof UnprivatizationRevisionError,
121 export const selectJoinedUnprivatizationState = createSelector(
122 [selectUnprivatizationState, selectMembers],
123 (unprivatizationState, membersState) => {
124 const result = getJoinedUnprivatizationMemberList(unprivatizationState, membersState);
125 const filteredResult = result.reduce<{
126 failures: (UnprivatizationMemberFailure & { member: MemberReadyForUnprivatization })[];
127 approval: (UnprivatizationMemberApproval & { member: MemberReadyForUnprivatizationApproval })[];
130 if (cur.type === 'error' && cur.revision) {
131 acc.failures.push(cur);
133 if (cur.type === 'approval') {
134 acc.approval.push(cur);
138 { failures: [], approval: [] }
142 loading: unprivatizationState.loading,
147 export const unprivatizeApprovalMembersHelper = ({
148 membersToUnprivatize,
150 membersToUnprivatize: MemberReadyForUnprivatizationApproval[];
153 membersToUpdate: Member[];
154 membersToError: { member: Member; error: any }[];
156 MembersState & OrganizationKeyState,
157 ProtonThunkArguments,
160 return async (dispatch, getState, extra) => {
161 const membersToUpdate: Member[] = [];
162 const membersToError: { member: Member; error: any }[] = [];
163 extra.eventManager.stop();
164 const api = getSilentApi(extra.api);
165 for (const member of membersToUnprivatize) {
167 const [organizationKey, memberAddresses] = await Promise.all([
168 dispatch(organizationKeyThunk()), // Fetch org key again to ensure it's up-to-date.
169 dispatch(getMemberAddresses({ member, retry: true })),
171 const payload = await unprivatizeMemberHelper({
173 ActivationToken: member.Unprivatization.ActivationToken,
174 PrivateKeys: member.Unprivatization.PrivateKeys,
177 verificationKeys: null,
178 organizationKey: organizationKey.privateKey,
180 await api(unprivatizeMemberKeysRoute(member.ID, payload));
181 const newMember = await getMember(api, member.ID);
182 membersToUpdate.push(newMember);
183 } catch (error: any) {
184 membersToError.push({ member, error });
187 extra.eventManager.start();
188 return { membersToUpdate, membersToError };
192 export const unprivatizeApprovalMembers = ({
193 membersToUnprivatize,
195 membersToUnprivatize: MemberReadyForUnprivatizationApproval[];
196 }): ThunkAction<Promise<Member[]>, MembersState & OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
197 return async (dispatch, getState) => {
198 if (!membersToUnprivatize.length) {
203 const oldState = selectUnprivatizationState(getState());
204 if (oldState.loading.approval) {
207 const newState = createNextState(oldState, (state) => {
208 state.loading.approval = true;
210 dispatch(setUnprivatizationState(newState));
213 const { membersToUpdate, membersToError } = await dispatch(
214 unprivatizeApprovalMembersHelper({ membersToUnprivatize })
217 membersToError.forEach(({ error }) => {
218 reportSentryError(error);
222 const oldState = selectUnprivatizationState(getState());
223 const newState = createNextState(oldState, (state) => {
224 state.loading.approval = false;
226 membersToUpdate.forEach((member) => {
227 state.members[member.ID] = { type: 'success' };
230 membersToError.forEach(({ member, error }) => {
231 state.members[member.ID] = getErrorState(error);
235 if (newState !== oldState) {
236 dispatch(setUnprivatizationState(newState));
240 membersToUpdate.forEach((member) => {
241 dispatch(upsertMember({ member }));
244 return membersToUpdate;
248 const getMembersToUnprivatize = ({
252 members: EnhancedMember[];
253 oldState: ReturnType<typeof selectUnprivatizationState>;
255 membersToApprove: Member[];
256 membersToDelete: string[];
257 membersToUnprivatize: MemberReadyForUnprivatization[];
259 const membersToDelete: string[] = [];
260 const membersToApprove: Member[] = [];
261 const membersToUnprivatize: MemberReadyForUnprivatization[] = [];
263 members.forEach((member) => {
264 const item = oldState.members[member.ID];
265 if (getMemberToUnprivatize(member)) {
267 membersToUnprivatize.push(member);
269 } else if (getMemberToUnprivatizeApproval(member)) {
271 membersToApprove.push(member);
273 // If there is a previous error and the user is no longer to unprivatize, delete it
274 } else if (item && item.type === 'error') {
275 membersToDelete.push(member.ID);
279 return { membersToUnprivatize, membersToDelete, membersToApprove };
282 export const unprivatizeMembersBackgroundHelper = ({
283 membersToUnprivatize,
284 verifyOutboundPublicKeys,
287 membersToUnprivatize: MemberReadyForUnprivatization[];
288 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
289 options?: Parameters<typeof unprivatizeMember>[0]['options'];
292 membersToUpdate: Member[];
293 membersToError: { member: Member; error: any }[];
295 MembersState & OrganizationKeyState,
296 ProtonThunkArguments,
299 return async (dispatch, getState, extra) => {
300 const membersToUpdate: Member[] = [];
301 const membersToError: { member: Member; error: any }[] = [];
303 extra.eventManager.stop();
304 const api = getSilentApi(extra.api);
305 for (const member of membersToUnprivatize) {
307 const [organizationKey, memberAddresses] = await Promise.all([
308 dispatch(organizationKeyThunk()), // Fetch org key again to ensure it's up-to-date.
309 dispatch(getMemberAddresses({ member, retry: true })),
311 const payload = await unprivatizeMember({
315 organizationKey: organizationKey.privateKey,
316 verifyOutboundPublicKeys,
319 await api(unprivatizeMemberKeysRoute(member.ID, payload));
320 const newMember = await getMember(api, member.ID);
321 membersToUpdate.push(newMember);
322 } catch (error: any) {
323 membersToError.push({ member, error });
326 extra.eventManager.start();
327 return { membersToUpdate, membersToError };
331 export const unprivatizeMembersBackground = ({
332 verifyOutboundPublicKeys,
336 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
337 options?: Parameters<typeof unprivatizeMember>[0]['options'];
341 members: EnhancedMember[];
345 members: MemberReadyForUnprivatization[];
347 }): ThunkAction<Promise<void>, MembersState & OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
348 return async (dispatch, getState) => {
349 if (!target.members.length) {
353 const oldState = selectUnprivatizationState(getState());
354 if (oldState.loading.automatic) {
358 const { membersToUnprivatize, membersToDelete, membersToApprove } =
359 target.type === 'action'
360 ? { membersToUnprivatize: target.members, membersToDelete: [], membersToApprove: [] }
361 : getMembersToUnprivatize({
362 members: target.members,
366 if (membersToUnprivatize.length || membersToApprove.length || membersToDelete.length) {
367 const newState = createNextState(oldState, (state) => {
368 state.loading.automatic = Boolean(membersToUnprivatize.length);
370 membersToApprove.forEach((member) => {
371 state.members[member.ID] = { type: 'approval' };
374 membersToDelete.forEach((memberID) => {
375 delete state.members[memberID];
379 if (oldState !== newState) {
380 dispatch(setUnprivatizationState(newState));
384 if (!membersToUnprivatize.length) {
388 const { membersToError, membersToUpdate } = await dispatch(
389 unprivatizeMembersBackgroundHelper({
390 membersToUnprivatize,
391 verifyOutboundPublicKeys,
396 membersToError.forEach(({ error }) => {
397 reportSentryError(error);
401 const oldState = selectUnprivatizationState(getState());
402 const newState = createNextState(oldState, (state) => {
403 state.loading.automatic = false;
405 membersToUpdate.forEach((member) => {
406 state.members[member.ID] = { type: 'success' };
409 membersToError.forEach(({ member, error }) => {
410 state.members[member.ID] = getErrorState(error);
414 if (newState !== oldState) {
415 dispatch(setUnprivatizationState(newState));
419 membersToUpdate.forEach((member) => {
420 dispatch(upsertMember({ member }));