1 import { PayloadAction, createSlice } from '@reduxjs/toolkit';
2 import { ThunkAction } from 'redux-thunk';
4 import { CacheType, getFetchedAt, getFetchedEphemeral, isNotStale } from '@proton/redux-utilities';
5 import { getFeatures, updateFeatureValue } from '@proton/shared/lib/api/features';
6 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
7 import { HOUR } from '@proton/shared/lib/constants';
8 import type { Api } from '@proton/shared/lib/interfaces';
9 import unique from '@proton/utils/unique';
11 import type { Feature, FeatureCode } from './interface';
13 interface FeatureState {
15 meta: { fetchedAt: number; fetchedEphemeral: boolean | undefined };
18 type FeaturesState = { [key in FeatureCode]?: FeatureState };
20 const codePromiseCache: { [key in FeatureCode]?: Promise<any> } = {};
22 export interface FeaturesReducerState {
23 features: FeaturesState;
26 export const selectFeatures = (state: FeaturesReducerState) => state.features;
28 export interface ThunkArguments {
32 const initialState = {} as FeaturesState;
34 export const featuresReducer = createSlice({
38 'fetch/fulfilled': (state, action: PayloadAction<{ codes: FeatureCode[]; features: FeaturesState }>) => {
41 ...action.payload.features,
44 'update/pending': (state, action: PayloadAction<{ code: FeatureCode; featureValue: any }>) => {
45 const old = state[action.payload.code];
47 state[action.payload.code] = {
51 Value: action.payload.featureValue,
56 'update/fulfilled': (state, action: PayloadAction<{ code: FeatureCode; feature: FeatureState }>) => {
57 const old = state[action.payload.code];
58 state[action.payload.code] = {
61 ...action.payload.feature.value,
63 meta: action.payload.feature.meta,
66 'update/rejected': (state, action: PayloadAction<{ code: FeatureCode; feature: FeatureState | undefined }>) => {
67 state[action.payload.code] = action.payload.feature;
72 export const isValidFeature = (
73 featureState: FeatureState | undefined,
75 ): featureState is FeatureState => {
76 if (cache === CacheType.None) {
80 featureState?.value !== undefined &&
81 (cache === CacheType.Stale || isNotStale(featureState.meta.fetchedAt, HOUR * 2))
88 type ThunkResult<Key extends FeatureCode> = { [key in Key]: Feature };
90 const defaultFeature = {} as Feature;
92 export const fetchFeatures = <T extends FeatureCode>(
98 Promise<ThunkResult<T>>,
101 ReturnType<(typeof featuresReducer.actions)['fetch/fulfilled']>
103 return async (dispatch, getState, extraArgument) => {
104 const featuresState = selectFeatures(getState());
106 const codesToFetch = codes.filter((code) => {
107 if (codePromiseCache[code] !== undefined) {
110 return !isValidFeature(featuresState[code], options?.cache);
113 if (codesToFetch.length) {
114 const promise = getSilentApi(extraArgument.api)<{
116 }>(getFeatures(codesToFetch)).then(({ Features }) => {
117 const fetchedAt = getFetchedAt();
118 const fetchedEphemeral = getFetchedEphemeral();
119 const result = Features.reduce<FeaturesState>(
121 acc[Feature.Code as FeatureCode] = { value: Feature, meta: { fetchedAt, fetchedEphemeral } };
124 codesToFetch.reduce<FeaturesState>((acc, code) => {
125 acc[code] = { value: defaultFeature, meta: { fetchedAt, fetchedEphemeral } };
129 dispatch(featuresReducer.actions['fetch/fulfilled']({ codes: codesToFetch, features: result }));
133 codesToFetch.forEach((code) => {
134 codePromiseCache[code] = promise;
138 delete codePromiseCache[code];
144 const promises = unique(codes.map((code) => codePromiseCache[code]).filter(Boolean));
145 if (promises.length) {
146 await Promise.all(promises);
149 const latestFeaturesState = selectFeatures(getState());
150 return Object.fromEntries(
151 codes.map((code) => {
152 return [code, latestFeaturesState[code]?.value || featuresState[code]?.value || defaultFeature];
158 export const updateFeature = (
163 FeaturesReducerState,
165 | ReturnType<(typeof featuresReducer.actions)['update/pending']>
166 | ReturnType<(typeof featuresReducer.actions)['update/fulfilled']>
167 | ReturnType<(typeof featuresReducer.actions)['update/rejected']>
169 return async (dispatch, getState, extraArgument) => {
170 const current = selectFeatures(getState())[code];
173 featuresReducer.actions['update/pending']({
178 const { Feature } = await getSilentApi(extraArgument.api)<{
180 }>(updateFeatureValue(code, value));
182 featuresReducer.actions['update/fulfilled']({
186 meta: { fetchedAt: getFetchedAt(), fetchedEphemeral: getFetchedEphemeral() },
192 dispatch(featuresReducer.actions['update/rejected']({ code, feature: current }));