Merge branch 'IDTEAM-remove-bf-ff' into 'main'
[ProtonMail-WebClient.git] / packages / account / organization / index.ts
blobe5e4453a13fbeb024441fe7587cce1cc7f06662d
1 import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
2 import { createSlice, miniSerializeError, original } from '@reduxjs/toolkit';
3 import type { ThunkAction } from 'redux-thunk';
5 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
6 import type { CacheType } from '@proton/redux-utilities';
7 import {
8     cacheHelper,
9     createPromiseStore,
10     getFetchedAt,
11     getFetchedEphemeral,
12     previousSelector,
13 } from '@proton/redux-utilities';
14 import { getIsMissingScopeError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
15 import { getOrganization, getOrganizationSettings } from '@proton/shared/lib/api/organization';
16 import { APPS } from '@proton/shared/lib/constants';
17 import { hasBit } from '@proton/shared/lib/helpers/bitset';
18 import updateObject from '@proton/shared/lib/helpers/updateObject';
19 import type { Organization, OrganizationSettings, OrganizationWithSettings, User } from '@proton/shared/lib/interfaces';
20 import { UserLockedFlags } from '@proton/shared/lib/interfaces';
21 import { isPaid } from '@proton/shared/lib/user/helpers';
23 import { serverEvent } from '../eventLoop';
24 import type { ModelState } from '../interface';
25 import { type UserState, userThunk } from '../user';
27 const name = 'organization' as const;
29 enum ValueType {
30     dummy,
31     complete,
34 export interface OrganizationState extends UserState {
35     [name]: ModelState<OrganizationWithSettings> & { meta: { type: ValueType } };
38 type SliceState = OrganizationState[typeof name];
39 type Model = NonNullable<SliceState['value']>;
41 export const selectOrganization = (state: OrganizationState) => state[name];
43 const canFetch = (user: User) => {
44     /*
45     After auto-downgrade admin user is downgraded to a free user, organization state is set to `Delinquent`
46     and the user gets into a locked state if they have members in their organizaion and .
47     In that case we want to refetch the organization to avoid getting FREE_ORGANIZATION object.
48     */
49     const isOrgAdminUserInLockedState = hasBit(user.LockedFlags, UserLockedFlags.ORG_ISSUE_FOR_PRIMARY_ADMIN);
50     return isPaid(user) || isOrgAdminUserInLockedState;
53 const freeOrganization = { Settings: {} } as unknown as OrganizationWithSettings;
55 const initialState: SliceState = {
56     value: undefined,
57     error: undefined,
58     meta: {
59         type: ValueType.dummy,
60         fetchedAt: 0,
61         fetchedEphemeral: undefined,
62     },
64 const slice = createSlice({
65     name,
66     initialState,
67     reducers: {
68         pending: (state) => {
69             state.error = undefined;
70         },
71         fulfilled: (state, action: PayloadAction<{ value: Model; type: ValueType }>) => {
72             state.value = action.payload.value;
73             state.error = undefined;
74             state.meta.type = action.payload.type;
75             state.meta.fetchedAt = getFetchedAt();
76             state.meta.fetchedEphemeral = getFetchedEphemeral();
77         },
78         rejected: (state, action) => {
79             state.error = action.payload;
80             state.meta.fetchedAt = getFetchedAt();
81         },
82         updateOrganizationSettings: (
83             state,
84             action: PayloadAction<{ value: Partial<OrganizationWithSettings['Settings']> }>
85         ) => {
86             if (!state.value) {
87                 return;
88             }
89             state.value.Settings = updateObject(state.value.Settings, action.payload.value);
90         },
91     },
92     extraReducers: (builder) => {
93         builder.addCase(serverEvent, (state, action) => {
94             if (!state.value) {
95                 return;
96             }
97             if (action.payload.Organization || action.payload.OrganizationSettings) {
98                 state.value = updateObject(state.value, {
99                     ...action.payload.Organization,
100                     ...(action.payload.OrganizationSettings
101                         ? { Settings: action.payload.OrganizationSettings }
102                         : undefined),
103                 });
104                 state.error = undefined;
105                 state.meta.type = ValueType.complete;
106             } else {
107                 const isFreeOrganization = original(state)?.meta.type === ValueType.dummy;
109                 if (!isFreeOrganization && action.payload.User && !canFetch(action.payload.User)) {
110                     // Do not get any organization update when user becomes unsubscribed.
111                     state.value = freeOrganization;
112                     state.error = undefined;
113                     state.meta.type = ValueType.dummy;
114                 }
116                 if (isFreeOrganization && action.payload.User && canFetch(action.payload.User)) {
117                     state.value = undefined;
118                     state.error = undefined;
119                     state.meta.type = ValueType.complete;
120                 }
121             }
122         });
123     },
126 const promiseStore = createPromiseStore<Model>();
127 const previous = previousSelector(selectOrganization);
129 const modelThunk = (options?: {
130     cache?: CacheType;
131 }): ThunkAction<Promise<Model>, OrganizationState, ProtonThunkArguments, UnknownAction> => {
132     return (dispatch, getState, extraArgument) => {
133         const select = () => {
134             return previous({ dispatch, getState, extraArgument, options });
135         };
136         const getPayload = async () => {
137             const user = await dispatch(userThunk());
138             const defaultValue = {
139                 value: freeOrganization,
140                 type: ValueType.dummy,
141             };
142             if (!canFetch(user)) {
143                 return defaultValue;
144             }
146             try {
147                 const defaultSettings = {
148                     ShowName: false,
149                     LogoID: null,
150                     ShowScribeWritingAssistant: true,
151                     VideoConferencingEnabled: false,
152                 };
154                 const [Organization, OrganizationSettings] = await Promise.all([
155                     extraArgument
156                         .api<{
157                             Organization: Organization;
158                         }>(getOrganization())
159                         .then(({ Organization }) => Organization),
160                     extraArgument.config.APP_NAME === APPS.PROTONACCOUNTLITE
161                         ? defaultSettings
162                         : extraArgument
163                               .api<OrganizationSettings>(getOrganizationSettings())
164                               .then(({ ShowName, LogoID, ShowScribeWritingAssistant, VideoConferencingEnabled }) => ({
165                                   ShowName,
166                                   LogoID,
167                                   ShowScribeWritingAssistant,
168                                   VideoConferencingEnabled,
169                               }))
170                               .catch(() => {
171                                   return defaultSettings;
172                               }),
173                 ]);
175                 const value = {
176                     ...Organization,
177                     Settings: OrganizationSettings,
178                 };
180                 return {
181                     value: value,
182                     type: ValueType.complete,
183                 };
184             } catch (e: any) {
185                 if (getIsMissingScopeError(e)) {
186                     return defaultValue;
187                 }
188                 throw e;
189             }
190         };
191         const cb = async () => {
192             try {
193                 dispatch(slice.actions.pending());
194                 const payload = await getPayload();
195                 dispatch(slice.actions.fulfilled(payload));
196                 return payload.value;
197             } catch (error) {
198                 dispatch(slice.actions.rejected(miniSerializeError(error)));
199                 throw error;
200             }
201         };
202         return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache });
203     };
206 export const organizationReducer = { [name]: slice.reducer };
207 export const organizationActions = slice.actions;
208 export const organizationThunk = modelThunk;
210 export const MAX_CHARS_API = {
211     ORG_NAME: 40,