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 { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './state/Store';
|
||||
import store, { persistor } from './state/Store';
|
||||
import { Router } from './Navigation';
|
||||
import './i18n/i18n';
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import LanguageGate from "./i18n/LanguageGate";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NativeBaseProvider theme={Theme}>
|
||||
<Router />
|
||||
</NativeBaseProvider>
|
||||
</QueryClientProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NativeBaseProvider theme={Theme}>
|
||||
<LanguageGate>
|
||||
<Router/>
|
||||
</LanguageGate>
|
||||
</NativeBaseProvider>
|
||||
</QueryClientProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
+14
-4
@@ -8,6 +8,8 @@ import { NavigationContainer } from '@react-navigation/native';
|
||||
import { useSelector } from './state/Store';
|
||||
import SongLobbyView from './views/SongLobbyView';
|
||||
import { translate } from './i18n/i18n';
|
||||
import { useQuery } from 'react-query';
|
||||
import API from './API';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
@@ -23,12 +25,20 @@ export const publicRoutes = <React.Fragment>
|
||||
</React.Fragment>;
|
||||
|
||||
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 (
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator>
|
||||
{isAuthentified ? protectedRoutes : publicRoutes}
|
||||
</Stack.Navigator>
|
||||
{isAuthentified && !userProfile.isError
|
||||
? <Stack.Navigator>
|
||||
{protectedRoutes}
|
||||
</Stack.Navigator>
|
||||
: <Stack.Navigator>
|
||||
{publicRoutes}
|
||||
</Stack.Navigator>
|
||||
}
|
||||
</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": {
|
||||
"@expo/vector-icons": "^13.0.0",
|
||||
"@react-native-async-storage/async-storage": "^1.17.11",
|
||||
"@react-navigation/native": "^6.0.11",
|
||||
"@react-navigation/native-stack": "^6.7.0",
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
@@ -25,6 +26,7 @@
|
||||
"expo": "~45.0.0",
|
||||
"expo-asset": "~8.5.0",
|
||||
"expo-dev-client": "~1.0.0",
|
||||
"expo-secure-store": "~11.2.0",
|
||||
"expo-status-bar": "~1.3.0",
|
||||
"format-duration": "^2.0.0",
|
||||
"i18next": "^21.8.16",
|
||||
@@ -43,6 +45,7 @@
|
||||
"react-native-testing-library": "^6.0.0",
|
||||
"react-native-web": "0.17.7",
|
||||
"react-redux": "^8.0.2",
|
||||
"redux-persist": "^6.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import i18n, { AvailableLanguages, DefaultLanguage } from "../i18n/i18n";
|
||||
import { AvailableLanguages, DefaultLanguage } from "../i18n/i18n";
|
||||
|
||||
|
||||
export const languageSlice = createSlice({
|
||||
@@ -10,11 +10,9 @@ export const languageSlice = createSlice({
|
||||
reducers: {
|
||||
useLanguage: (state, action: PayloadAction<AvailableLanguages>) => {
|
||||
state.value = action.payload;
|
||||
i18n.changeLanguage(state.value);
|
||||
},
|
||||
resetLanguage: (state) => {
|
||||
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 settingsReduder from '../state/SettingsSlice';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import languageReducer from './LanguageSlice';
|
||||
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({
|
||||
reducer: {
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage: AsyncStorage
|
||||
}
|
||||
|
||||
let store = configureStore({
|
||||
reducer: persistCombineReducers(persistConfig, {
|
||||
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
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
@@ -18,4 +34,5 @@ export type AppDispatch = typeof store.dispatch;
|
||||
export const useDispatch: () => AppDispatch = reduxDispatch
|
||||
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);
|
||||
return translate("loggedIn");
|
||||
} 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 { unsetAccessToken } from '../state/UserSlice';
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useSelector } from '../state/Store';
|
||||
import { RootState, useSelector } from '../state/Store';
|
||||
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();
|
||||
|
||||
@@ -48,8 +49,8 @@ const MainView = ({navigation}) => {
|
||||
|
||||
const PreferencesView = ({navigation}) => {
|
||||
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 (
|
||||
<Center style={{ flex: 1}}>
|
||||
<Heading style={{ textAlign: "center" }}>
|
||||
@@ -60,14 +61,16 @@ const PreferencesView = ({navigation}) => {
|
||||
</Button>
|
||||
|
||||
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
|
||||
<Select selectedValue={undefined}
|
||||
placeholder={'Theme'}
|
||||
<Select selectedValue={settings.colorScheme}
|
||||
placeholder={'Theme'}
|
||||
style={{ alignSelf: 'center'}}
|
||||
// onValueChange={(itemValue, itemIndex) => switch themes}
|
||||
>
|
||||
<Select.Item label={translate('dark')} value='dark'/>
|
||||
<Select.Item label={translate('light')} value='light'/>
|
||||
<Select.Item label={translate('system')} value='system'/>
|
||||
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>
|
||||
</View>
|
||||
|
||||
@@ -75,10 +78,8 @@ const PreferencesView = ({navigation}) => {
|
||||
<Select selectedValue={language}
|
||||
placeholder={translate('langBtn')}
|
||||
style={{ alignSelf: 'center'}}
|
||||
onValueChange={(itemValue: AvailableLanguages, itemIndex) => {
|
||||
let newLanguage = DefaultLanguage;
|
||||
newLanguage = itemValue;Heading
|
||||
dispatch(useLanguage(newLanguage));
|
||||
onValueChange={(itemValue) => {
|
||||
dispatch(useLanguage(itemValue as AvailableLanguages));
|
||||
}}>
|
||||
<Select.Item label='Français' value='fr'/>
|
||||
<Select.Item label='English' value='en'/>
|
||||
@@ -88,12 +89,12 @@ const PreferencesView = ({navigation}) => {
|
||||
</View>
|
||||
|
||||
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
|
||||
<Select selectedValue={undefined}
|
||||
<Select selectedValue={settings.preferedLevel}
|
||||
placeholder={ translate('diffBtn') }
|
||||
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('medium') } value='medium'/>
|
||||
<Select.Item label={ translate('hard') } value='hard'/>
|
||||
@@ -102,12 +103,16 @@ const PreferencesView = ({navigation}) => {
|
||||
|
||||
<View style={{margin: 20}}>
|
||||
<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 style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
|
||||
<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.FilledTrack/>
|
||||
</Slider.Track>
|
||||
@@ -116,11 +121,11 @@ const PreferencesView = ({navigation}) => {
|
||||
</View>
|
||||
|
||||
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
|
||||
<Select selectedValue={undefined}
|
||||
<Select selectedValue={settings.preferedInputName}
|
||||
placeholder={'Device'}
|
||||
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_1' value='1'/>
|
||||
<Select.Item label='Mic_2' value='2'/>
|
||||
@@ -131,6 +136,8 @@ const PreferencesView = ({navigation}) => {
|
||||
}
|
||||
|
||||
const NotificationsView = ({navigation}) => {
|
||||
const dispatch = useDispatch();
|
||||
const settings: SettingsState = useSelector((state: RootState) => state.settings);
|
||||
return (
|
||||
<Center style={{ flex: 1, justifyContent: 'center' }}>
|
||||
|
||||
@@ -142,19 +149,27 @@ const NotificationsView = ({navigation}) => {
|
||||
</Button>
|
||||
<View style={{margin: 20}} >
|
||||
<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 style={{margin: 20}}>
|
||||
<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 style={{margin: 20}}>
|
||||
<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 style={{margin: 20}}>
|
||||
<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>
|
||||
</Center>
|
||||
)
|
||||
|
||||
@@ -2221,6 +2221,13 @@
|
||||
"@react-aria/ssr" "^3.0.1"
|
||||
"@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":
|
||||
version "7.0.3"
|
||||
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"
|
||||
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:
|
||||
version "1.3.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.4"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714"
|
||||
|
||||
Reference in New Issue
Block a user