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';
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;
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) => {
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.
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 = {
59 type: ValueType.dummy,
61 fetchedEphemeral: undefined,
64 const slice = createSlice({
69 state.error = undefined;
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();
78 rejected: (state, action) => {
79 state.error = action.payload;
80 state.meta.fetchedAt = getFetchedAt();
82 updateOrganizationSettings: (
84 action: PayloadAction<{ value: Partial<OrganizationWithSettings['Settings']> }>
89 state.value.Settings = updateObject(state.value.Settings, action.payload.value);
92 extraReducers: (builder) => {
93 builder.addCase(serverEvent, (state, action) => {
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 }
104 state.error = undefined;
105 state.meta.type = ValueType.complete;
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;
116 if (isFreeOrganization && action.payload.User && canFetch(action.payload.User)) {
117 state.value = undefined;
118 state.error = undefined;
119 state.meta.type = ValueType.complete;
126 const promiseStore = createPromiseStore<Model>();
127 const previous = previousSelector(selectOrganization);
129 const modelThunk = (options?: {
131 }): ThunkAction<Promise<Model>, OrganizationState, ProtonThunkArguments, UnknownAction> => {
132 return (dispatch, getState, extraArgument) => {
133 const select = () => {
134 return previous({ dispatch, getState, extraArgument, options });
136 const getPayload = async () => {
137 const user = await dispatch(userThunk());
138 const defaultValue = {
139 value: freeOrganization,
140 type: ValueType.dummy,
142 if (!canFetch(user)) {
147 const defaultSettings = {
150 ShowScribeWritingAssistant: true,
151 VideoConferencingEnabled: false,
154 const [Organization, OrganizationSettings] = await Promise.all([
157 Organization: Organization;
158 }>(getOrganization())
159 .then(({ Organization }) => Organization),
160 extraArgument.config.APP_NAME === APPS.PROTONACCOUNTLITE
163 .api<OrganizationSettings>(getOrganizationSettings())
164 .then(({ ShowName, LogoID, ShowScribeWritingAssistant, VideoConferencingEnabled }) => ({
167 ShowScribeWritingAssistant,
168 VideoConferencingEnabled,
171 return defaultSettings;
177 Settings: OrganizationSettings,
182 type: ValueType.complete,
185 if (getIsMissingScopeError(e)) {
191 const cb = async () => {
193 dispatch(slice.actions.pending());
194 const payload = await getPayload();
195 dispatch(slice.actions.fulfilled(payload));
196 return payload.value;
198 dispatch(slice.actions.rejected(miniSerializeError(error)));
202 return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache });
206 export const organizationReducer = { [name]: slice.reducer };
207 export const organizationActions = slice.actions;
208 export const organizationThunk = modelThunk;
210 export const MAX_CHARS_API = {