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 { queryDomains } from '@proton/shared/lib/api/domains';
15 import { getIsMissingScopeError } from '@proton/shared/lib/api/helpers/apiErrorHelper';
16 import queryPages from '@proton/shared/lib/api/helpers/queryPages';
17 import updateCollection from '@proton/shared/lib/helpers/updateCollection';
18 import type { Domain, User } from '@proton/shared/lib/interfaces';
19 import { isAdmin } from '@proton/shared/lib/user/helpers';
21 import { serverEvent } from '../eventLoop';
22 import type { ModelState } from '../interface';
23 import type { UserState } from '../user';
24 import { userThunk } from '../user';
26 const name = 'domains' as const;
33 export interface DomainsState extends UserState {
34 [name]: ModelState<Domain[]> & { meta: { type: ValueType } };
37 type SliceState = DomainsState[typeof name];
38 type Model = NonNullable<SliceState['value']>;
40 export const selectDomains = (state: DomainsState) => state.domains;
42 const canFetch = (user: User) => {
45 const freeDomains: Domain[] = [];
47 const initialState: SliceState = {
51 fetchedEphemeral: undefined,
53 type: ValueType.dummy,
56 const slice = createSlice({
61 state.error = undefined;
63 fulfilled: (state, action: PayloadAction<{ value: Model; type: ValueType }>) => {
64 state.value = action.payload.value;
65 state.error = undefined;
66 state.meta.type = action.payload.type;
67 state.meta.fetchedAt = getFetchedAt();
68 state.meta.fetchedEphemeral = getFetchedEphemeral();
70 rejected: (state, action) => {
71 state.error = action.payload;
72 state.meta.fetchedAt = getFetchedAt();
75 extraReducers: (builder) => {
76 builder.addCase(serverEvent, (state, action) => {
80 const isFreeDomains = original(state)?.meta?.type === ValueType.dummy;
81 if (action.payload.Domains && !isFreeDomains) {
82 state.value = updateCollection({
84 events: action.payload.Domains,
87 state.error = undefined;
88 state.meta.type = ValueType.complete;
90 if (!isFreeDomains && action.payload.User && !canFetch(action.payload.User)) {
91 // Do not get any domain update when user becomes unsubscribed.
92 state.value = freeDomains;
93 state.error = undefined;
94 state.meta.type = ValueType.dummy;
97 if (isFreeDomains && action.payload.User && canFetch(action.payload.User)) {
98 state.value = undefined;
99 state.error = undefined;
100 state.meta.type = ValueType.complete;
107 const promiseStore = createPromiseStore<Model>();
109 const previous = previousSelector(selectDomains);
111 const modelThunk = (options?: {
113 }): ThunkAction<Promise<Model>, DomainsState, ProtonThunkArguments, UnknownAction> => {
114 return (dispatch, getState, extraArgument) => {
115 const select = () => {
116 return previous({ dispatch, getState, extraArgument, options });
118 const getPayload = async () => {
119 const user = await dispatch(userThunk());
120 const defaultValue = {
122 type: ValueType.dummy,
124 if (!canFetch(user)) {
128 const pages = await queryPages((page, pageSize) => {
129 return extraArgument.api<{ Domains: Domain[]; Total: number }>(
136 const value = pages.flatMap(({ Domains }) => Domains);
139 type: ValueType.complete,
142 if (getIsMissingScopeError(e)) {
148 const cb = async () => {
150 dispatch(slice.actions.pending());
151 const payload = await getPayload();
152 dispatch(slice.actions.fulfilled(payload));
153 return payload.value;
155 dispatch(slice.actions.rejected(miniSerializeError(error)));
159 return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache });
163 export const domainsReducer = { [name]: slice.reducer };
164 export const domainsThunk = modelThunk;