Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / account / organization / index.ts
blob1409306c357190f77734fc77b8112af607123f06
1 import { PayloadAction, UnknownAction, createSlice, miniSerializeError, original } from '@reduxjs/toolkit';
2 import { ThunkAction } from 'redux-thunk';
4 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
5 import {
6     CacheType,
7     cacheHelper,
8     createPromiseStore,
9     getFetchedAt,
10     getFetchedEphemeral,
11     previousSelector,
12 } from '@proton/redux-utilities';
13 import { getOrganization, getOrganizationSettings } from '@proton/shared/lib/api/organization';
14 import { APPS } from '@proton/shared/lib/constants';
15 import { hasBit } from '@proton/shared/lib/helpers/bitset';
16 import updateObject from '@proton/shared/lib/helpers/updateObject';
17 import {
18     type Organization,
19     type OrganizationSettings,
20     OrganizationWithSettings,
21     type User,
22     UserLockedFlags,
23 } from '@proton/shared/lib/interfaces';
24 import { isPaid } from '@proton/shared/lib/user/helpers';
26 import { serverEvent } from '../eventLoop';
27 import type { ModelState } from '../interface';
28 import { type UserState, userThunk } from '../user';
30 const name = 'organization' as const;
32 enum ValueType {
33     dummy,
34     complete,
37 export interface OrganizationState extends UserState {
38     [name]: ModelState<OrganizationWithSettings> & { meta: { type: ValueType } };
41 type SliceState = OrganizationState[typeof name];
42 type Model = NonNullable<SliceState['value']>;
44 export const selectOrganization = (state: OrganizationState) => state[name];
46 const canFetch = (user: User) => {
47     /*
48     After auto-downgrade admin user is downgraded to a free user, organization state is set to `Delinquent`
49     and the user gets into a locked state if they have members in their organizaion and .
50     In that case we want to refetch the organization to avoid getting FREE_ORGANIZATION object.
51     */
52     const isOrgAdminUserInLockedState = hasBit(user.LockedFlags, UserLockedFlags.ORG_ISSUE_FOR_PRIMARY_ADMIN);
53     return isPaid(user) || isOrgAdminUserInLockedState;
56 const freeOrganization = { Settings: {} } as unknown as OrganizationWithSettings;
58 const initialState: SliceState = {
59     value: undefined,
60     error: undefined,
61     meta: {
62         type: ValueType.dummy,
63         fetchedAt: 0,
64         fetchedEphemeral: undefined,
65     },
67 const slice = createSlice({
68     name,
69     initialState,
70     reducers: {
71         pending: (state) => {
72             state.error = undefined;
73         },
74         fulfilled: (state, action: PayloadAction<{ value: Model; type: ValueType }>) => {
75             state.value = action.payload.value;
76             state.error = undefined;
77             state.meta.type = action.payload.type;
78             state.meta.fetchedAt = getFetchedAt();
79             state.meta.fetchedEphemeral = getFetchedEphemeral();
80         },
81         rejected: (state, action) => {
82             state.error = action.payload;
83             state.meta.fetchedAt = getFetchedAt();
84         },
85     },
86     extraReducers: (builder) => {
87         builder.addCase(serverEvent, (state, action) => {
88             if (!state.value) {
89                 return;
90             }
91             if (action.payload.Organization || action.payload.OrganizationSettings) {
92                 state.value = updateObject(state.value, {
93                     ...action.payload.Organization,
94                     ...(action.payload.OrganizationSettings
95                         ? { Settings: action.payload.OrganizationSettings }
96                         : undefined),
97                 });
98                 state.error = undefined;
99                 state.meta.type = ValueType.complete;
100             } else {
101                 const isFreeOrganization = original(state)?.meta.type === ValueType.dummy;
103                 if (!isFreeOrganization && action.payload.User && !canFetch(action.payload.User)) {
104                     // Do not get any organization update when user becomes unsubscribed.
105                     state.value = freeOrganization;
106                     state.error = undefined;
107                     state.meta.type = ValueType.dummy;
108                 }
110                 if (isFreeOrganization && action.payload.User && canFetch(action.payload.User)) {
111                     state.value = undefined;
112                     state.error = undefined;
113                     state.meta.type = ValueType.complete;
114                 }
115             }
116         });
117     },
120 const promiseStore = createPromiseStore<Model>();
121 const previous = previousSelector(selectOrganization);
123 const modelThunk = (options?: {
124     cache?: CacheType;
125 }): ThunkAction<Promise<Model>, OrganizationState, ProtonThunkArguments, UnknownAction> => {
126     return (dispatch, getState, extraArgument) => {
127         const select = () => {
128             return previous({ dispatch, getState, extraArgument, options });
129         };
130         const getPayload = async () => {
131             const user = await dispatch(userThunk());
132             if (!canFetch(user)) {
133                 return { value: freeOrganization, type: ValueType.dummy };
134             }
136             const defaultSettings = {
137                 ShowName: false,
138                 LogoID: null,
139             };
141             const [Organization, OrganizationSettings] = await Promise.all([
142                 extraArgument
143                     .api<{
144                         Organization: Organization;
145                     }>(getOrganization())
146                     .then(({ Organization }) => Organization),
147                 extraArgument.config.APP_NAME === APPS.PROTONACCOUNTLITE
148                     ? defaultSettings
149                     : extraArgument
150                           .api<OrganizationSettings>(getOrganizationSettings())
151                           .then(({ ShowName, LogoID }) => ({ ShowName, LogoID }))
152                           .catch(() => {
153                               return defaultSettings;
154                           }),
155             ]);
157             const value = {
158                 ...Organization,
159                 Settings: OrganizationSettings,
160             };
162             return {
163                 value: value,
164                 type: ValueType.complete,
165             };
166         };
167         const cb = async () => {
168             try {
169                 dispatch(slice.actions.pending());
170                 const payload = await getPayload();
171                 dispatch(slice.actions.fulfilled(payload));
172                 return payload.value;
173             } catch (error) {
174                 dispatch(slice.actions.rejected(miniSerializeError(error)));
175                 throw error;
176             }
177         };
178         return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache });
179     };
182 export const organizationReducer = { [name]: slice.reducer };
183 export const organizationThunk = modelThunk;
185 export const MAX_CHARS_API = {
186     ORG_NAME: 40,