Front: settings persistance (#108)

* Front: Add peristance dependencies

* Front: Fix Cross-platform persistance

* Front: Create Settings Slice

* Front: Use Redux State for settings

* Front: Check if access token is still valid

* Front: Create Language Gate to set correct language at startup

* Front: BEtter handling of Access Token validity
This commit is contained in:
Arthur Jamet
2022-11-26 14:18:06 +00:00
committed by GitHub
parent 8546c86332
commit 55526dbadc
10 changed files with 179 additions and 47 deletions
+12 -6
View File
@@ -3,20 +3,26 @@ import Theme from './Theme';
import React from 'react'; import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import store from './state/Store'; import store, { persistor } from './state/Store';
import { Router } from './Navigation'; import { Router } from './Navigation';
import './i18n/i18n'; import './i18n/i18n';
import { PersistGate } from "redux-persist/integration/react";
import LanguageGate from "./i18n/LanguageGate";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export default function App() { export default function App() {
return ( return (
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <PersistGate loading={null} persistor={persistor}>
<NativeBaseProvider theme={Theme}> <QueryClientProvider client={queryClient}>
<Router /> <NativeBaseProvider theme={Theme}>
</NativeBaseProvider> <LanguageGate>
</QueryClientProvider> <Router/>
</LanguageGate>
</NativeBaseProvider>
</QueryClientProvider>
</PersistGate>
</Provider> </Provider>
); );
} }
+14 -4
View File
@@ -8,6 +8,8 @@ import { NavigationContainer } from '@react-navigation/native';
import { useSelector } from './state/Store'; import { useSelector } from './state/Store';
import SongLobbyView from './views/SongLobbyView'; import SongLobbyView from './views/SongLobbyView';
import { translate } from './i18n/i18n'; import { translate } from './i18n/i18n';
import { useQuery } from 'react-query';
import API from './API';
const Stack = createNativeStackNavigator(); const Stack = createNativeStackNavigator();
@@ -23,12 +25,20 @@ export const publicRoutes = <React.Fragment>
</React.Fragment>; </React.Fragment>;
export const Router = () => { export const Router = () => {
const isAuthentified = useSelector((state) => state.user.accessToken !== undefined) const isAuthentified = useSelector((state) => state.user.accessToken !== undefined);
const userProfile = useQuery(['user', 'me'], () => API.getUserInfo(), {
enabled: isAuthentified
});
return ( return (
<NavigationContainer> <NavigationContainer>
<Stack.Navigator> {isAuthentified && !userProfile.isError
{isAuthentified ? protectedRoutes : publicRoutes} ? <Stack.Navigator>
</Stack.Navigator> {protectedRoutes}
</Stack.Navigator>
: <Stack.Navigator>
{publicRoutes}
</Stack.Navigator>
}
</NavigationContainer> </NavigationContainer>
) )
} }
+18
View File
@@ -0,0 +1,18 @@
import { RootState, useSelector } from "../state/Store";
import i18n from "./i18n";
type LanguageGateProps = {
children: any;
}
/**
* Gate to handle language update at startup and on every dispatch
* @param props the children to render
*/
const LanguageGate = (props: LanguageGateProps) => {
const language = useSelector((state: RootState) => state.language.value);
i18n.changeLanguage(language);
return props.children;
}
export default LanguageGate;
+3
View File
@@ -14,6 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^13.0.0", "@expo/vector-icons": "^13.0.0",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-navigation/native": "^6.0.11", "@react-navigation/native": "^6.0.11",
"@react-navigation/native-stack": "^6.7.0", "@react-navigation/native-stack": "^6.7.0",
"@reduxjs/toolkit": "^1.8.3", "@reduxjs/toolkit": "^1.8.3",
@@ -25,6 +26,7 @@
"expo": "~45.0.0", "expo": "~45.0.0",
"expo-asset": "~8.5.0", "expo-asset": "~8.5.0",
"expo-dev-client": "~1.0.0", "expo-dev-client": "~1.0.0",
"expo-secure-store": "~11.2.0",
"expo-status-bar": "~1.3.0", "expo-status-bar": "~1.3.0",
"format-duration": "^2.0.0", "format-duration": "^2.0.0",
"i18next": "^21.8.16", "i18next": "^21.8.16",
@@ -43,6 +45,7 @@
"react-native-testing-library": "^6.0.0", "react-native-testing-library": "^6.0.0",
"react-native-web": "0.17.7", "react-native-web": "0.17.7",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"redux-persist": "^6.0.0",
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
+1 -3
View File
@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import i18n, { AvailableLanguages, DefaultLanguage } from "../i18n/i18n"; import { AvailableLanguages, DefaultLanguage } from "../i18n/i18n";
export const languageSlice = createSlice({ export const languageSlice = createSlice({
@@ -10,11 +10,9 @@ export const languageSlice = createSlice({
reducers: { reducers: {
useLanguage: (state, action: PayloadAction<AvailableLanguages>) => { useLanguage: (state, action: PayloadAction<AvailableLanguages>) => {
state.value = action.payload; state.value = action.payload;
i18n.changeLanguage(state.value);
}, },
resetLanguage: (state) => { resetLanguage: (state) => {
state.value = DefaultLanguage; state.value = DefaultLanguage;
i18n.changeLanguage(DefaultLanguage);
}, },
}, },
}); });
+36
View File
@@ -0,0 +1,36 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export type SettingsState = {
colorScheme: "dark" | "light" | "system",
enablePushNotifications: boolean,
enableMailNotifications: boolean,
enableLessongsReminders: boolean,
enableReleaseAlerts: boolean,
preferedLevel: 'easy' | 'medium' | 'hard',
colorBlind: boolean,
micLevel: number,
preferedInputName?: string
}
export const settingsSlice = createSlice({
name: 'settings',
initialState: {
settings: <SettingsState>{
enablePushNotifications: true,
enableMailNotifications: true,
enableLessongsReminders: true,
enableReleaseAlerts: true,
preferedLevel: 'easy',
colorBlind: false,
micLevel: 50,
colorScheme: "system"
},
},
reducers: {
updateSettings: (state, action: PayloadAction<Partial<SettingsState>>) => {
state.settings = { ...state.settings, ...action.payload };
}
}
});
export const { updateSettings } = settingsSlice.actions;
export default settingsSlice.reducer;
+22 -5
View File
@@ -1,14 +1,30 @@
import userReducer from '../state/UserSlice'; import userReducer from '../state/UserSlice';
import settingsReduder from '../state/SettingsSlice';
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import languageReducer from './LanguageSlice'; import languageReducer from './LanguageSlice';
import { TypedUseSelectorHook, useDispatch as reduxDispatch, useSelector as reduxSelector } from 'react-redux' import { TypedUseSelectorHook, useDispatch as reduxDispatch, useSelector as reduxSelector } from 'react-redux'
import { persistStore, persistCombineReducers, FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from "redux-persist";
import AsyncStorage from '@react-native-async-storage/async-storage';
const store = configureStore({ const persistConfig = {
reducer: { key: 'root',
storage: AsyncStorage
}
let store = configureStore({
reducer: persistCombineReducers(persistConfig, {
user: userReducer, user: userReducer,
language: languageReducer language: languageReducer,
}, settings: settingsReduder
}),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
}) })
let persistor = persistStore(store);
// Infer the `RootState` and `AppDispatch` types from the store itself // Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState> export type RootState = ReturnType<typeof store.getState>
@@ -18,4 +34,5 @@ export type AppDispatch = typeof store.dispatch;
export const useDispatch: () => AppDispatch = reduxDispatch export const useDispatch: () => AppDispatch = reduxDispatch
export const useSelector: TypedUseSelectorHook<RootState> = reduxSelector export const useSelector: TypedUseSelectorHook<RootState> = reduxSelector
export default store export default store
export { persistor }
+1 -1
View File
@@ -13,7 +13,7 @@ const hanldeSignin = async (username: string, password: string, apiSetter: (acce
apiSetter(apiAccess); apiSetter(apiAccess);
return translate("loggedIn"); return translate("loggedIn");
} catch (error) { } catch (error) {
return "Username of password incorrect"; return "Username or password incorrect";
} }
}; };
+43 -28
View File
@@ -4,9 +4,10 @@ import { Center, Button, Text, Switch, Slider, Select, Heading } from "native-ba
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { unsetAccessToken } from '../state/UserSlice'; import { unsetAccessToken } from '../state/UserSlice';
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { useSelector } from '../state/Store'; import { RootState, useSelector } from '../state/Store';
import { useLanguage } from "../state/LanguageSlice"; import { useLanguage } from "../state/LanguageSlice";
import { AvailableLanguages, DefaultLanguage, translate, Translate } from "../i18n/i18n"; import { SettingsState, updateSettings } from '../state/SettingsSlice';
import { AvailableLanguages, translate, Translate } from "../i18n/i18n";
const SettingsStack = createNativeStackNavigator(); const SettingsStack = createNativeStackNavigator();
@@ -48,8 +49,8 @@ const MainView = ({navigation}) => {
const PreferencesView = ({navigation}) => { const PreferencesView = ({navigation}) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const language: AvailableLanguages = useSelector((state) => state.language.value); const language: AvailableLanguages = useSelector((state: RootState) => state.language.value);
const settings = useSelector((state: RootState) => (state.settings.settings as SettingsState));
return ( return (
<Center style={{ flex: 1}}> <Center style={{ flex: 1}}>
<Heading style={{ textAlign: "center" }}> <Heading style={{ textAlign: "center" }}>
@@ -60,14 +61,16 @@ const PreferencesView = ({navigation}) => {
</Button> </Button>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}> <View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={undefined} <Select selectedValue={settings.colorScheme}
placeholder={'Theme'} placeholder={'Theme'}
style={{ alignSelf: 'center'}} style={{ alignSelf: 'center'}}
// onValueChange={(itemValue, itemIndex) => switch themes} onValueChange={(newColorScheme) => {
> dispatch(updateSettings({ colorScheme: newColorScheme as any }))
<Select.Item label={translate('dark')} value='dark'/> }}
<Select.Item label={translate('light')} value='light'/> >
<Select.Item label={translate('system')} value='system'/> <Select.Item label={ translate('dark') } value='dark'/>
<Select.Item label={ translate('light') } value='light'/>
<Select.Item label={ translate('system') } value='system'/>
</Select> </Select>
</View> </View>
@@ -75,10 +78,8 @@ const PreferencesView = ({navigation}) => {
<Select selectedValue={language} <Select selectedValue={language}
placeholder={translate('langBtn')} placeholder={translate('langBtn')}
style={{ alignSelf: 'center'}} style={{ alignSelf: 'center'}}
onValueChange={(itemValue: AvailableLanguages, itemIndex) => { onValueChange={(itemValue) => {
let newLanguage = DefaultLanguage; dispatch(useLanguage(itemValue as AvailableLanguages));
newLanguage = itemValue;Heading
dispatch(useLanguage(newLanguage));
}}> }}>
<Select.Item label='Français' value='fr'/> <Select.Item label='Français' value='fr'/>
<Select.Item label='English' value='en'/> <Select.Item label='English' value='en'/>
@@ -88,12 +89,12 @@ const PreferencesView = ({navigation}) => {
</View> </View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}> <View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={undefined} <Select selectedValue={settings.preferedLevel}
placeholder={ translate('diffBtn') } placeholder={ translate('diffBtn') }
style={{ height: 50, width: 150, alignSelf: 'center'}} style={{ height: 50, width: 150, alignSelf: 'center'}}
// onValueChange={(itemValue, itemIndex) => change level} onValueChange={(itemValue) => {
> dispatch(updateSettings({ preferedLevel: itemValue as any }));
}}>
<Select.Item label={ translate('easy') } value='easy'/> <Select.Item label={ translate('easy') } value='easy'/>
<Select.Item label={ translate('medium') } value='medium'/> <Select.Item label={ translate('medium') } value='medium'/>
<Select.Item label={ translate('hard') } value='hard'/> <Select.Item label={ translate('hard') } value='hard'/>
@@ -102,12 +103,16 @@ const PreferencesView = ({navigation}) => {
<View style={{margin: 20}}> <View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Color blind mode</Text> <Text style={{ textAlign: "center" }}>Color blind mode</Text>
<Switch style={{ alignSelf: 'center'}} colorScheme="primary"/> <Switch style={{ alignSelf: 'center'}} value={settings.colorBlind} colorScheme="primary"
onValueChange={(enabled) => { dispatch(updateSettings({ colorBlind: enabled })) }}
/>
</View> </View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}> <View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Text style={{ textAlign: "center" }}>Mic volume</Text> <Text style={{ textAlign: "center" }}>Mic volume</Text>
<Slider defaultValue={50} minValue={0} maxValue={1000} accessibilityLabel="hello world" step={10}> <Slider defaultValue={settings.micLevel} minValue={0} maxValue={1000} accessibilityLabel="hello world" step={10}
onChangeEnd={(value) => { dispatch(updateSettings({ micLevel: value })) }}
>
<Slider.Track> <Slider.Track>
<Slider.FilledTrack/> <Slider.FilledTrack/>
</Slider.Track> </Slider.Track>
@@ -116,11 +121,11 @@ const PreferencesView = ({navigation}) => {
</View> </View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}> <View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={undefined} <Select selectedValue={settings.preferedInputName}
placeholder={'Device'} placeholder={'Device'}
style={{ height: 50, width: 150, alignSelf: 'center'}} style={{ height: 50, width: 150, alignSelf: 'center'}}
// onValueChange={(itemValue, itemIndex) => change device} onValueChange={(itemValue: string) => { dispatch(updateSettings({ preferedInputName: itemValue })) }}
> >
<Select.Item label='Mic_0' value='0'/> <Select.Item label='Mic_0' value='0'/>
<Select.Item label='Mic_1' value='1'/> <Select.Item label='Mic_1' value='1'/>
<Select.Item label='Mic_2' value='2'/> <Select.Item label='Mic_2' value='2'/>
@@ -131,6 +136,8 @@ const PreferencesView = ({navigation}) => {
} }
const NotificationsView = ({navigation}) => { const NotificationsView = ({navigation}) => {
const dispatch = useDispatch();
const settings: SettingsState = useSelector((state: RootState) => state.settings);
return ( return (
<Center style={{ flex: 1, justifyContent: 'center' }}> <Center style={{ flex: 1, justifyContent: 'center' }}>
@@ -142,19 +149,27 @@ const NotificationsView = ({navigation}) => {
</Button> </Button>
<View style={{margin: 20}} > <View style={{margin: 20}} >
<Text style={{ textAlign: "center" }}>Push notifications</Text> <Text style={{ textAlign: "center" }}>Push notifications</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/> <Switch value={settings.enablePushNotifications} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enablePushNotifications: value })) }}
/>
</View> </View>
<View style={{margin: 20}}> <View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Email notifications</Text> <Text style={{ textAlign: "center" }}>Email notifications</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/> <Switch value={settings.enableMailNotifications} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enableMailNotifications: value })) }}
/>
</View> </View>
<View style={{margin: 20}}> <View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Training reminder</Text> <Text style={{ textAlign: "center" }}>Training reminder</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/> <Switch value={settings.enableLessongsReminders} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enableLessongsReminders: value })) }}
/>
</View> </View>
<View style={{margin: 20}}> <View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>New songs</Text> <Text style={{ textAlign: "center" }}>New songs</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/> <Switch value={settings.enableReleaseAlerts} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enableReleaseAlerts: value })) }}
/>
</View> </View>
</Center> </Center>
) )
+29
View File
@@ -2221,6 +2221,13 @@
"@react-aria/ssr" "^3.0.1" "@react-aria/ssr" "^3.0.1"
"@react-aria/utils" "^3.3.0" "@react-aria/utils" "^3.3.0"
"@react-native-async-storage/async-storage@^1.17.11":
version "1.17.11"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.11.tgz#7ec329c1b9f610e344602e806b04d7c928a2341d"
integrity sha512-bzs45n5HNcDq6mxXnSsOHysZWn1SbbebNxldBXCQs8dSvF8Aor9KCdpm+TpnnGweK3R6diqsT8lFhX77VX0NFw==
dependencies:
merge-options "^3.0.4"
"@react-native-community/cli-debugger-ui@^7.0.3": "@react-native-community/cli-debugger-ui@^7.0.3":
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-7.0.3.tgz#3eeeacc5a43513cbcae56e5e965d77726361bcb4" resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-7.0.3.tgz#3eeeacc5a43513cbcae56e5e965d77726361bcb4"
@@ -4845,6 +4852,11 @@ expo-modules-core@0.9.2:
compare-versions "^3.4.0" compare-versions "^3.4.0"
invariant "^2.2.4" invariant "^2.2.4"
expo-secure-store@~11.2.0:
version "11.2.0"
resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-11.2.0.tgz#c2c8fdcac39c5e1828a8fea45765028a11625575"
integrity sha512-PlmDplx9QNqaTVKNLgqSurRhzYf6YbVTTiSKX5JlEMWgOiBTz77Nh6mpMMjI1jwrvtz9grD+CT2AlDEGXe+Ovg==
expo-status-bar@~1.3.0: expo-status-bar@~1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.3.0.tgz#d71fd0b880ea201905f5dd8abcd18db7476c9f03" resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.3.0.tgz#d71fd0b880ea201905f5dd8abcd18db7476c9f03"
@@ -5790,6 +5802,11 @@ is-path-inside@^3.0.2:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@^2.0.3, is-plain-object@^2.0.4: is-plain-object@^2.0.3, is-plain-object@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
@@ -6938,6 +6955,13 @@ memory-cache@~0.2.0:
resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a" resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a"
integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA== integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==
merge-options@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
dependencies:
is-plain-obj "^2.1.0"
merge-stream@^2.0.0: merge-stream@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -8456,6 +8480,11 @@ recast@^0.20.4:
source-map "~0.6.1" source-map "~0.6.1"
tslib "^2.0.1" tslib "^2.0.1"
redux-persist@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
redux-thunk@^2.4.1: redux-thunk@^2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714"