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:
+12
-6
@@ -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
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,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);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user