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';
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';
19 type OrganizationSettings,
20 OrganizationWithSettings,
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;
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) => {
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.
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 = {
62 type: ValueType.dummy,
64 fetchedEphemeral: undefined,
67 const slice = createSlice({
72 state.error = undefined;
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();
81 rejected: (state, action) => {
82 state.error = action.payload;
83 state.meta.fetchedAt = getFetchedAt();
86 extraReducers: (builder) => {
87 builder.addCase(serverEvent, (state, action) => {
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 }
98 state.error = undefined;
99 state.meta.type = ValueType.complete;
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;
110 if (isFreeOrganization && action.payload.User && canFetch(action.payload.User)) {
111 state.value = undefined;
112 state.error = undefined;
113 state.meta.type = ValueType.complete;
120 const promiseStore = createPromiseStore<Model>();
121 const previous = previousSelector(selectOrganization);
123 const modelThunk = (options?: {
125 }): ThunkAction<Promise<Model>, OrganizationState, ProtonThunkArguments, UnknownAction> => {
126 return (dispatch, getState, extraArgument) => {
127 const select = () => {
128 return previous({ dispatch, getState, extraArgument, options });
130 const getPayload = async () => {
131 const user = await dispatch(userThunk());
132 if (!canFetch(user)) {
133 return { value: freeOrganization, type: ValueType.dummy };
136 const defaultSettings = {
141 const [Organization, OrganizationSettings] = await Promise.all([
144 Organization: Organization;
145 }>(getOrganization())
146 .then(({ Organization }) => Organization),
147 extraArgument.config.APP_NAME === APPS.PROTONACCOUNTLITE
150 .api<OrganizationSettings>(getOrganizationSettings())
151 .then(({ ShowName, LogoID }) => ({ ShowName, LogoID }))
153 return defaultSettings;
159 Settings: OrganizationSettings,
164 type: ValueType.complete,
167 const cb = async () => {
169 dispatch(slice.actions.pending());
170 const payload = await getPayload();
171 dispatch(slice.actions.fulfilled(payload));
172 return payload.value;
174 dispatch(slice.actions.rejected(miniSerializeError(error)));
178 return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache });
182 export const organizationReducer = { [name]: slice.reducer };
183 export const organizationThunk = modelThunk;
185 export const MAX_CHARS_API = {