Merge branch 'INDA-330-pii-update' into 'main'
[ProtonMail-WebClient.git] / packages / account / domains / index.ts
blob0444e75184e274b120f14885fbb48738d1aa20e2
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 { 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;
28 enum ValueType {
29     dummy,
30     complete,
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) => {
43     return isAdmin(user);
45 const freeDomains: Domain[] = [];
47 const initialState: SliceState = {
48     value: undefined,
49     error: undefined,
50     meta: {
51         fetchedEphemeral: undefined,
52         fetchedAt: 0,
53         type: ValueType.dummy,
54     },
56 const slice = createSlice({
57     name,
58     initialState,
59     reducers: {
60         pending: (state) => {
61             state.error = undefined;
62         },
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();
69         },
70         rejected: (state, action) => {
71             state.error = action.payload;
72             state.meta.fetchedAt = getFetchedAt();
73         },
74     },
75     extraReducers: (builder) => {
76         builder.addCase(serverEvent, (state, action) => {
77             if (!state.value) {
78                 return;
79             }
80             const isFreeDomains = original(state)?.meta?.type === ValueType.dummy;
81             if (action.payload.Domains && !isFreeDomains) {
82                 state.value = updateCollection({
83                     model: state.value,
84                     events: action.payload.Domains,
85                     itemKey: 'Domain',
86                 });
87                 state.error = undefined;
88                 state.meta.type = ValueType.complete;
89             } else {
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;
95                 }
97                 if (isFreeDomains && action.payload.User && canFetch(action.payload.User)) {
98                     state.value = undefined;
99                     state.error = undefined;
100                     state.meta.type = ValueType.complete;
101                 }
102             }
103         });
104     },
107 const promiseStore = createPromiseStore<Model>();
109 const previous = previousSelector(selectDomains);
111 const modelThunk = (options?: {
112     cache?: CacheType;
113 }): ThunkAction<Promise<Model>, DomainsState, ProtonThunkArguments, UnknownAction> => {
114     return (dispatch, getState, extraArgument) => {
115         const select = () => {
116             return previous({ dispatch, getState, extraArgument, options });
117         };
118         const getPayload = async () => {
119             const user = await dispatch(userThunk());
120             const defaultValue = {
121                 value: freeDomains,
122                 type: ValueType.dummy,
123             };
124             if (!canFetch(user)) {
125                 return defaultValue;
126             }
127             try {
128                 const pages = await queryPages((page, pageSize) => {
129                     return extraArgument.api<{ Domains: Domain[]; Total: number }>(
130                         queryDomains({
131                             Page: page,
132                             PageSize: pageSize,
133                         })
134                     );
135                 });
136                 const value = pages.flatMap(({ Domains }) => Domains);
137                 return {
138                     value,
139                     type: ValueType.complete,
140                 };
141             } catch (e: any) {
142                 if (getIsMissingScopeError(e)) {
143                     return defaultValue;
144                 }
145                 throw e;
146             }
147         };
148         const cb = async () => {
149             try {
150                 dispatch(slice.actions.pending());
151                 const payload = await getPayload();
152                 dispatch(slice.actions.fulfilled(payload));
153                 return payload.value;
154             } catch (error) {
155                 dispatch(slice.actions.rejected(miniSerializeError(error)));
156                 throw error;
157             }
158         };
159         return cacheHelper({ store: promiseStore, select, cb, cache: options?.cache });
160     };
163 export const domainsReducer = { [name]: slice.reducer };
164 export const domainsThunk = modelThunk;