diff --git a/App.tsx b/App.tsx
index 9d5a93f..bcea3ee 100644
--- a/App.tsx
+++ b/App.tsx
@@ -21,7 +21,6 @@ import React from 'react';
import { LogBox, Platform } from 'react-native';
import { setSafeBounceHeight } from 'react-navigation-collapsible';
import SplashScreen from 'react-native-splash-screen';
-import ConnectionManager from './src/managers/ConnectionManager';
import type { ParsedUrlDataType } from './src/utils/URLHandler';
import URLHandler from './src/utils/URLHandler';
import initLocales from './src/utils/Locales';
@@ -48,6 +47,8 @@ import {
ProxiwashPreferencesProvider,
} from './src/components/providers/PreferencesProvider';
import MainApp from './src/screens/MainApp';
+import LoginProvider from './src/components/providers/LoginProvider';
+import { retrieveLoginToken } from './src/utils/loginToken';
// Native optimizations https://reactnavigation.org/docs/react-native-screens
// Crashes app when navigating away from webview on android 9+
@@ -67,6 +68,7 @@ type StateType = {
proxiwash: ProxiwashPreferencesType;
mascot: MascotPreferencesType;
};
+ loginToken?: string;
};
export default class App extends React.Component<{}, StateType> {
@@ -88,6 +90,7 @@ export default class App extends React.Component<{}, StateType> {
proxiwash: defaultProxiwashPreferences,
mascot: defaultMascotPreferences,
},
+ loginToken: undefined,
};
initLocales();
this.navigatorRef = React.createRef();
@@ -136,10 +139,11 @@ export default class App extends React.Component<{}, StateType> {
| PlanexPreferencesType
| ProxiwashPreferencesType
| MascotPreferencesType
- | void
+ | string
+ | undefined
>
) => {
- const [general, planex, proxiwash, mascot] = values;
+ const [general, planex, proxiwash, mascot, token] = values;
this.setState({
isLoading: false,
initialPreferences: {
@@ -148,6 +152,7 @@ export default class App extends React.Component<{}, StateType> {
proxiwash: proxiwash as ProxiwashPreferencesType,
mascot: mascot as MascotPreferencesType,
},
+ loginToken: token as string | undefined,
});
SplashScreen.hide();
};
@@ -175,7 +180,7 @@ export default class App extends React.Component<{}, StateType> {
Object.values(MascotPreferenceKeys),
defaultMascotPreferences
),
- ConnectionManager.getInstance().recoverLogin(),
+ retrieveLoginToken(),
])
.then(this.onLoadFinished)
.catch(this.onLoadFinished);
@@ -202,11 +207,13 @@ export default class App extends React.Component<{}, StateType> {
-
+
+
+
diff --git a/src/components/Amicale/Login/LoginForm.tsx b/src/components/Amicale/Login/LoginForm.tsx
new file mode 100644
index 0000000..4fbcf60
--- /dev/null
+++ b/src/components/Amicale/Login/LoginForm.tsx
@@ -0,0 +1,231 @@
+import React, { useRef, useState } from 'react';
+import {
+ Image,
+ StyleSheet,
+ View,
+ TextInput as RNTextInput,
+} from 'react-native';
+import {
+ Button,
+ Card,
+ HelperText,
+ TextInput,
+ useTheme,
+} from 'react-native-paper';
+import i18n from 'i18n-js';
+import GENERAL_STYLES from '../../../constants/Styles';
+
+type Props = {
+ loading: boolean;
+ onSubmit: (email: string, password: string) => void;
+ onHelpPress: () => void;
+ onResetPasswordPress: () => void;
+};
+
+const ICON_AMICALE = require('../../../assets/amicale.png');
+
+const styles = StyleSheet.create({
+ card: {
+ marginTop: 'auto',
+ marginBottom: 'auto',
+ },
+ header: {
+ fontSize: 36,
+ marginBottom: 48,
+ },
+ text: {
+ color: '#ffffff',
+ },
+ buttonContainer: {
+ flexWrap: 'wrap',
+ },
+ lockButton: {
+ marginRight: 'auto',
+ marginBottom: 20,
+ },
+ sendButton: {
+ marginLeft: 'auto',
+ },
+});
+
+const emailRegex = /^.+@.+\..+$/;
+
+/**
+ * Checks if the entered email is valid (matches the regex)
+ *
+ * @returns {boolean}
+ */
+function isEmailValid(email: string): boolean {
+ return emailRegex.test(email);
+}
+
+/**
+ * Checks if the user has entered a password
+ *
+ * @returns {boolean}
+ */
+function isPasswordValid(password: string): boolean {
+ return password !== '';
+}
+
+export default function LoginForm(props: Props) {
+ const theme = useTheme();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isEmailValidated, setIsEmailValidated] = useState(false);
+ const [isPasswordValidated, setIsPasswordValidated] = useState(false);
+ const passwordRef = useRef(null);
+ /**
+ * Checks if we should tell the user his email is invalid.
+ * We should only show this if his email is invalid and has been checked when un-focusing the input
+ *
+ * @returns {boolean|boolean}
+ */
+ const shouldShowEmailError = () => {
+ return isEmailValidated && !isEmailValid(email);
+ };
+
+ /**
+ * Checks if we should tell the user his password is invalid.
+ * We should only show this if his password is invalid and has been checked when un-focusing the input
+ *
+ * @returns {boolean|boolean}
+ */
+ const shouldShowPasswordError = () => {
+ return isPasswordValidated && !isPasswordValid(password);
+ };
+
+ const onEmailSubmit = () => {
+ if (passwordRef.current) {
+ passwordRef.current.focus();
+ }
+ };
+
+ /**
+ * The user has unfocused the input, his email is ready to be validated
+ */
+ const validateEmail = () => setIsEmailValidated(true);
+
+ /**
+ * The user has unfocused the input, his password is ready to be validated
+ */
+ const validatePassword = () => setIsPasswordValidated(true);
+
+ const onEmailChange = (value: string) => {
+ if (isEmailValidated) {
+ setIsEmailValidated(false);
+ }
+ setEmail(value);
+ };
+
+ const onPasswordChange = (value: string) => {
+ if (isPasswordValidated) {
+ setIsPasswordValidated(false);
+ }
+ setPassword(value);
+ };
+
+ const shouldEnableLogin = () => {
+ return isEmailValid(email) && isPasswordValid(password) && !props.loading;
+ };
+
+ const onSubmit = () => {
+ if (shouldEnableLogin()) {
+ props.onSubmit(email, password);
+ }
+ };
+
+ return (
+
+ (
+
+ )}
+ />
+
+
+
+
+ {i18n.t('screens.login.emailError')}
+
+
+
+ {i18n.t('screens.login.passwordError')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Amicale/LogoutDialog.tsx b/src/components/Amicale/LogoutDialog.tsx
index 6e060a3..8d2759c 100644
--- a/src/components/Amicale/LogoutDialog.tsx
+++ b/src/components/Amicale/LogoutDialog.tsx
@@ -20,8 +20,7 @@
import * as React from 'react';
import i18n from 'i18n-js';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
-import ConnectionManager from '../../managers/ConnectionManager';
-import { useNavigation } from '@react-navigation/native';
+import { useLogout } from '../../utils/logout';
type PropsType = {
visible: boolean;
@@ -29,19 +28,12 @@ type PropsType = {
};
function LogoutDialog(props: PropsType) {
- const navigation = useNavigation();
+ const onLogout = useLogout();
+ // Use a loading dialog as it can take some time to update the context
const onClickAccept = async (): Promise => {
return new Promise((resolve: () => void) => {
- ConnectionManager.getInstance()
- .disconnect()
- .then(() => {
- navigation.reset({
- index: 0,
- routes: [{ name: 'main' }],
- });
- props.onDismiss();
- resolve();
- });
+ onLogout();
+ resolve();
});
};
diff --git a/src/components/Amicale/Profile/ProfileClubCard.tsx b/src/components/Amicale/Profile/ProfileClubCard.tsx
new file mode 100644
index 0000000..efb7876
--- /dev/null
+++ b/src/components/Amicale/Profile/ProfileClubCard.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { Card, Avatar, Divider, useTheme, List } from 'react-native-paper';
+import i18n from 'i18n-js';
+import { FlatList, StyleSheet } from 'react-native';
+import { ProfileClubType } from '../../../screens/Amicale/ProfileScreen';
+import { useNavigation } from '@react-navigation/core';
+
+type Props = {
+ clubs?: Array;
+};
+
+const styles = StyleSheet.create({
+ card: {
+ margin: 10,
+ },
+ icon: {
+ backgroundColor: 'transparent',
+ },
+});
+
+export default function ProfileClubCard(props: Props) {
+ const theme = useTheme();
+ const navigation = useNavigation();
+
+ const clubKeyExtractor = (item: ProfileClubType) => item.name;
+
+ const getClubListItem = ({ item }: { item: ProfileClubType }) => {
+ const onPress = () =>
+ navigation.navigate('club-information', { clubId: item.id });
+ let description = i18n.t('screens.profile.isMember');
+ let icon = (leftProps: {
+ color: string;
+ style: {
+ marginLeft: number;
+ marginRight: number;
+ marginVertical?: number;
+ };
+ }) => (
+
+ );
+ if (item.is_manager) {
+ description = i18n.t('screens.profile.isManager');
+ icon = (leftProps) => (
+
+ );
+ }
+ return (
+
+ );
+ };
+
+ function getClubList(list: Array | undefined) {
+ if (!list) {
+ return null;
+ }
+
+ list.sort((a) => (a.is_manager ? -1 : 1));
+ return (
+
+ );
+ }
+
+ return (
+
+ (
+
+ )}
+ />
+
+
+ {getClubList(props.clubs)}
+
+
+ );
+}
diff --git a/src/components/Amicale/Profile/ProfileMembershipCard.tsx b/src/components/Amicale/Profile/ProfileMembershipCard.tsx
new file mode 100644
index 0000000..1b11902
--- /dev/null
+++ b/src/components/Amicale/Profile/ProfileMembershipCard.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { Avatar, Card, List, useTheme } from 'react-native-paper';
+import i18n from 'i18n-js';
+import { StyleSheet } from 'react-native';
+
+type Props = {
+ valid?: boolean;
+};
+
+const styles = StyleSheet.create({
+ card: {
+ margin: 10,
+ },
+ icon: {
+ backgroundColor: 'transparent',
+ },
+});
+
+export default function ProfileMembershipCard(props: Props) {
+ const theme = useTheme();
+ const state = props.valid === true;
+ return (
+
+ (
+
+ )}
+ />
+
+
+ (
+
+ )}
+ />
+
+
+
+ );
+}
diff --git a/src/components/Amicale/Profile/ProfilePersonalCard.tsx b/src/components/Amicale/Profile/ProfilePersonalCard.tsx
new file mode 100644
index 0000000..2c8f3b6
--- /dev/null
+++ b/src/components/Amicale/Profile/ProfilePersonalCard.tsx
@@ -0,0 +1,110 @@
+import { useNavigation } from '@react-navigation/core';
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import {
+ Avatar,
+ Button,
+ Card,
+ Divider,
+ List,
+ useTheme,
+} from 'react-native-paper';
+import Urls from '../../../constants/Urls';
+import { ProfileDataType } from '../../../screens/Amicale/ProfileScreen';
+import i18n from 'i18n-js';
+
+type Props = {
+ profile?: ProfileDataType;
+};
+
+const styles = StyleSheet.create({
+ card: {
+ margin: 10,
+ },
+ icon: {
+ backgroundColor: 'transparent',
+ },
+ editButton: {
+ marginLeft: 'auto',
+ },
+ mascot: {
+ width: 60,
+ },
+ title: {
+ marginLeft: 10,
+ },
+});
+
+function getFieldValue(field?: string): string {
+ return field ? field : i18n.t('screens.profile.noData');
+}
+
+export default function ProfilePersonalCard(props: Props) {
+ const { profile } = props;
+ const theme = useTheme();
+ const navigation = useNavigation();
+
+ function getPersonalListItem(field: string | undefined, icon: string) {
+ const title = field != null ? getFieldValue(field) : ':(';
+ const subtitle = field != null ? '' : getFieldValue(field);
+ return (
+ (
+
+ )}
+ />
+ );
+ }
+
+ return (
+
+ (
+
+ )}
+ />
+
+
+
+
+ {i18n.t('screens.profile.personalInformation')}
+
+ {getPersonalListItem(profile?.birthday, 'cake-variant')}
+ {getPersonalListItem(profile?.phone, 'phone')}
+ {getPersonalListItem(profile?.email, 'email')}
+ {getPersonalListItem(profile?.branch, 'school')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Amicale/Profile/ProfileWelcomeCard.tsx b/src/components/Amicale/Profile/ProfileWelcomeCard.tsx
new file mode 100644
index 0000000..ca564ca
--- /dev/null
+++ b/src/components/Amicale/Profile/ProfileWelcomeCard.tsx
@@ -0,0 +1,81 @@
+import { useNavigation } from '@react-navigation/core';
+import React from 'react';
+import { Button, Card, Divider, Paragraph } from 'react-native-paper';
+import Mascot, { MASCOT_STYLE } from '../../Mascot/Mascot';
+import i18n from 'i18n-js';
+import { StyleSheet } from 'react-native';
+import CardList from '../../Lists/CardList/CardList';
+import { getAmicaleServices, SERVICES_KEY } from '../../../utils/Services';
+
+type Props = {
+ firstname?: string;
+};
+
+const styles = StyleSheet.create({
+ card: {
+ margin: 10,
+ },
+ editButton: {
+ marginLeft: 'auto',
+ },
+ mascot: {
+ width: 60,
+ },
+ title: {
+ marginLeft: 10,
+ },
+});
+
+function ProfileWelcomeCard(props: Props) {
+ const navigation = useNavigation();
+ return (
+
+ (
+
+ )}
+ titleStyle={styles.title}
+ />
+
+
+ {i18n.t('screens.profile.welcomeDescription')}
+
+ {i18n.t('screens.profile.welcomeFeedback')}
+
+
+
+
+
+
+ );
+}
+
+export default React.memo(
+ ProfileWelcomeCard,
+ (pp, np) => pp.firstname === np.firstname
+);
diff --git a/src/components/Amicale/Vote/VoteSelect.tsx b/src/components/Amicale/Vote/VoteSelect.tsx
index f0d1f81..fc290c0 100644
--- a/src/components/Amicale/Vote/VoteSelect.tsx
+++ b/src/components/Amicale/Vote/VoteSelect.tsx
@@ -17,30 +17,23 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
+import React, { useState } from 'react';
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
import { FlatList, StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
-import ConnectionManager from '../../../managers/ConnectionManager';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog';
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests';
+import { useAuthenticatedRequest } from '../../../context/loginContext';
-type PropsType = {
+type Props = {
teams: Array;
onVoteSuccess: () => void;
onVoteError: () => void;
};
-type StateType = {
- selectedTeam: string;
- voteDialogVisible: boolean;
- errorDialogVisible: boolean;
- currentError: ApiRejectType;
-};
-
const styles = StyleSheet.create({
card: {
margin: 10,
@@ -50,118 +43,98 @@ const styles = StyleSheet.create({
},
});
-export default class VoteSelect extends React.PureComponent<
- PropsType,
- StateType
-> {
- constructor(props: PropsType) {
- super(props);
- this.state = {
- selectedTeam: 'none',
- voteDialogVisible: false,
- errorDialogVisible: false,
- currentError: { status: REQUEST_STATUS.SUCCESS },
- };
- }
+function VoteSelect(props: Props) {
+ const [selectedTeam, setSelectedTeam] = useState('none');
+ const [voteDialogVisible, setVoteDialogVisible] = useState(false);
+ const [currentError, setCurrentError] = useState({
+ status: REQUEST_STATUS.SUCCESS,
+ });
+ const request = useAuthenticatedRequest('elections/vote', {
+ team: parseInt(selectedTeam, 10),
+ });
- onVoteSelectionChange = (teamName: string): void =>
- this.setState({ selectedTeam: teamName });
+ const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
- voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
-
- voteRenderItem = ({ item }: { item: VoteTeamType }) => (
+ const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
);
- showVoteDialog = (): void => this.setState({ voteDialogVisible: true });
+ const showVoteDialog = () => setVoteDialogVisible(true);
- onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false });
+ const onVoteDialogDismiss = () => setVoteDialogVisible(false);
- onVoteDialogAccept = async (): Promise => {
+ const onVoteDialogAccept = async (): Promise => {
return new Promise((resolve: () => void) => {
- const { state } = this;
- ConnectionManager.getInstance()
- .authenticatedRequest('elections/vote', {
- team: parseInt(state.selectedTeam, 10),
- })
+ request()
.then(() => {
- this.onVoteDialogDismiss();
- const { props } = this;
+ onVoteDialogDismiss();
props.onVoteSuccess();
resolve();
})
.catch((error: ApiRejectType) => {
- this.onVoteDialogDismiss();
- this.showErrorDialog(error);
+ onVoteDialogDismiss();
+ setCurrentError(error);
resolve();
});
});
};
- showErrorDialog = (error: ApiRejectType): void =>
- this.setState({
- errorDialogVisible: true,
- currentError: error,
- });
-
- onErrorDialogDismiss = () => {
- this.setState({ errorDialogVisible: false });
- const { props } = this;
+ const onErrorDialogDismiss = () => {
+ setCurrentError({ status: REQUEST_STATUS.SUCCESS });
props.onVoteError();
};
- render() {
- const { state, props } = this;
- return (
-
-
- (
-
- )}
- />
-
-
-
-
-
-
-
-
-
-
+
+ (
+
+ )}
/>
-
-
- );
- }
+
+
+
+
+
+
+
+
+
+
+
+
+ );
}
+
+export default VoteSelect;
diff --git a/src/components/Lists/Equipment/EquipmentListItem.tsx b/src/components/Lists/Equipment/EquipmentListItem.tsx
index f76414a..9797643 100644
--- a/src/components/Lists/Equipment/EquipmentListItem.tsx
+++ b/src/components/Lists/Equipment/EquipmentListItem.tsx
@@ -20,7 +20,6 @@
import * as React from 'react';
import { Avatar, List, useTheme } from 'react-native-paper';
import i18n from 'i18n-js';
-import { StackNavigationProp } from '@react-navigation/stack';
import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import {
getFirstEquipmentAvailability,
@@ -29,9 +28,9 @@ import {
} from '../../../utils/EquipmentBooking';
import { StyleSheet } from 'react-native';
import GENERAL_STYLES from '../../../constants/Styles';
+import { useNavigation } from '@react-navigation/native';
type PropsType = {
- navigation: StackNavigationProp;
userDeviceRentDates: [string, string] | null;
item: DeviceType;
height: number;
@@ -48,7 +47,8 @@ const styles = StyleSheet.create({
function EquipmentListItem(props: PropsType) {
const theme = useTheme();
- const { item, userDeviceRentDates, navigation, height } = props;
+ const navigation = useNavigation();
+ const { item, userDeviceRentDates, height } = props;
const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
diff --git a/src/components/Screens/RequestScreen.tsx b/src/components/Screens/RequestScreen.tsx
index 6d6550d..f886c75 100644
--- a/src/components/Screens/RequestScreen.tsx
+++ b/src/components/Screens/RequestScreen.tsx
@@ -11,7 +11,7 @@ import i18n from 'i18n-js';
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import { StackNavigationProp } from '@react-navigation/stack';
import { MainRoutes } from '../../navigation/MainNavigator';
-import ConnectionManager from '../../managers/ConnectionManager';
+import { useLogout } from '../../utils/logout';
export type RequestScreenProps = {
request: () => Promise;
@@ -44,6 +44,7 @@ type Props = RequestScreenProps;
const MIN_REFRESH_TIME = 3 * 1000;
export default function RequestScreen(props: Props) {
+ const onLogout = useLogout();
const navigation = useNavigation>();
const route = useRoute();
const refreshInterval = useRef();
@@ -103,13 +104,10 @@ export default function RequestScreen(props: Props) {
useEffect(() => {
if (isErrorCritical(code)) {
- ConnectionManager.getInstance()
- .disconnect()
- .then(() => {
- navigation.replace(MainRoutes.Login, { nextScreen: route.name });
- });
+ onLogout();
+ navigation.replace(MainRoutes.Login, { nextScreen: route.name });
}
- }, [code, navigation, route]);
+ }, [code, navigation, route, onLogout]);
if (data === undefined && loading && props.showLoading !== false) {
return ;
diff --git a/src/components/providers/LoginProvider.tsx b/src/components/providers/LoginProvider.tsx
new file mode 100644
index 0000000..f355cd6
--- /dev/null
+++ b/src/components/providers/LoginProvider.tsx
@@ -0,0 +1,27 @@
+import React, { useState } from 'react';
+import { LoginContext, LoginContextType } from '../../context/loginContext';
+
+type Props = {
+ children: React.ReactChild;
+ initialToken: string | undefined;
+};
+
+export default function LoginProvider(props: Props) {
+ const setLogin = (token: string | undefined) => {
+ setLoginState((prevState) => ({
+ ...prevState,
+ token,
+ }));
+ };
+
+ const [loginState, setLoginState] = useState({
+ token: props.initialToken,
+ setLogin: setLogin,
+ });
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/src/context/loginContext.ts b/src/context/loginContext.ts
new file mode 100644
index 0000000..4792281
--- /dev/null
+++ b/src/context/loginContext.ts
@@ -0,0 +1,46 @@
+import React, { useContext } from 'react';
+import { apiRequest } from '../utils/WebData';
+
+export type LoginContextType = {
+ token: string | undefined;
+ setLogin: (token: string | undefined) => void;
+};
+
+export const LoginContext = React.createContext({
+ token: undefined,
+ setLogin: () => undefined,
+});
+
+/**
+ * Hook used to retrieve the user token and puid.
+ * @returns Login context with token and puid to undefined if user is not logged in
+ */
+export function useLogin() {
+ return useContext(LoginContext);
+}
+
+/**
+ * Checks if the user is connected
+ * @returns True if the user is connected
+ */
+export function useLoginState() {
+ const { token } = useLogin();
+ return token !== undefined;
+}
+
+/**
+ * Gets the current user token.
+ * @returns The token, or empty string if the user is not logged in
+ */
+export function useLoginToken() {
+ const { token } = useLogin();
+ return token ? token : '';
+}
+
+export function useAuthenticatedRequest(
+ path: string,
+ params?: { [key: string]: any }
+) {
+ const token = useLoginToken();
+ return () => apiRequest(path, 'POST', params, token);
+}
diff --git a/src/managers/ConnectionManager.ts b/src/managers/ConnectionManager.ts
deleted file mode 100644
index ee11ff2..0000000
--- a/src/managers/ConnectionManager.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (c) 2019 - 2020 Arnaud Vergnet.
- *
- * This file is part of Campus INSAT.
- *
- * Campus INSAT is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Campus INSAT is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Campus INSAT. If not, see .
- */
-
-import * as Keychain from 'react-native-keychain';
-import { REQUEST_STATUS } from '../utils/Requests';
-import type { ApiDataLoginType, ApiRejectType } from '../utils/WebData';
-import { apiRequest } from '../utils/WebData';
-
-/**
- * champ: error
- *
- * 0 : SUCCESS -> pas d'erreurs
- * 1 : BAD_CREDENTIALS -> email ou mdp invalide
- * 2 : BAD_TOKEN -> session expirée
- * 3 : NO_CONSENT
- * 403 : FORBIDDEN -> accès a la ressource interdit
- * 500 : SERVER_ERROR -> pb coté serveur
- */
-
-const AUTH_PATH = 'password';
-
-export default class ConnectionManager {
- static instance: ConnectionManager | null = null;
-
- private token: string | null;
-
- constructor() {
- this.token = null;
- }
-
- /**
- * Gets this class instance or create one if none is found
- *
- * @returns {ConnectionManager}
- */
- static getInstance(): ConnectionManager {
- if (ConnectionManager.instance == null) {
- ConnectionManager.instance = new ConnectionManager();
- }
- return ConnectionManager.instance;
- }
-
- /**
- * Gets the current token
- *
- * @returns {string | null}
- */
- getToken(): string | null {
- return this.token;
- }
-
- /**
- * Tries to recover login token from the secure keychain
- *
- * @returns Promise
- */
- async recoverLogin(): Promise {
- return new Promise((resolve: () => void) => {
- const token = this.getToken();
- if (token != null) {
- resolve();
- } else {
- Keychain.getGenericPassword()
- .then((data: Keychain.UserCredentials | false) => {
- if (data && data.password != null) {
- this.token = data.password;
- }
- resolve();
- })
- .catch(() => resolve());
- }
- });
- }
-
- /**
- * Check if the user has a valid token
- *
- * @returns {boolean}
- */
- isLoggedIn(): boolean {
- return this.getToken() !== null;
- }
-
- /**
- * Saves the login token in the secure keychain
- *
- * @param email
- * @param token
- * @returns Promise
- */
- async saveLogin(_email: string, token: string): Promise {
- return new Promise((resolve: () => void, reject: () => void) => {
- Keychain.setGenericPassword('token', token)
- .then(() => {
- this.token = token;
- resolve();
- })
- .catch((): void => reject());
- });
- }
-
- /**
- * Deletes the login token from the keychain
- *
- * @returns Promise
- */
- async disconnect(): Promise {
- return new Promise((resolve: () => void, reject: () => void) => {
- Keychain.resetGenericPassword()
- .then(() => {
- this.token = null;
- resolve();
- })
- .catch((): void => reject());
- });
- }
-
- /**
- * Sends the given login and password to the api.
- * If the combination is valid, the login token is received and saved in the secure keychain.
- * If not, the promise is rejected with the corresponding error code.
- *
- * @param email
- * @param password
- * @returns Promise
- */
- async connect(email: string, password: string): Promise {
- return new Promise(
- (resolve: () => void, reject: (error: ApiRejectType) => void) => {
- const data = {
- email,
- password,
- };
- apiRequest(AUTH_PATH, 'POST', data)
- .then((response: ApiDataLoginType) => {
- if (response.token != null) {
- this.saveLogin(email, response.token)
- .then(() => resolve())
- .catch(() =>
- reject({
- status: REQUEST_STATUS.TOKEN_SAVE,
- })
- );
- } else {
- reject({
- status: REQUEST_STATUS.SERVER_ERROR,
- });
- }
- })
- .catch((err) => {
- reject(err);
- });
- }
- );
- }
-
- /**
- * Sends an authenticated request with the login token to the API
- *
- * @param path
- * @param params
- * @returns Promise
- */
- async authenticatedRequest(
- path: string,
- params?: { [key: string]: any }
- ): Promise {
- return new Promise(
- (
- resolve: (response: T) => void,
- reject: (error: ApiRejectType) => void
- ) => {
- if (this.getToken() !== null) {
- const data = {
- ...params,
- token: this.getToken(),
- };
- apiRequest(path, 'POST', data)
- .then((response: T) => resolve(response))
- .catch(reject);
- } else {
- reject({
- status: REQUEST_STATUS.TOKEN_RETRIEVE,
- });
- }
- }
- );
- }
-}
diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx b/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx
index 65cd4b0..e0b14b5 100644
--- a/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx
+++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.tsx
@@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
+import React, { useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native';
import {
Avatar,
@@ -25,20 +25,21 @@ import {
Card,
Chip,
Paragraph,
- withTheme,
+ useTheme,
} from 'react-native-paper';
import i18n from 'i18n-js';
-import { StackNavigationProp } from '@react-navigation/stack';
import CustomHTML from '../../../components/Overrides/CustomHTML';
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
import type { ClubCategoryType, ClubType } from './ClubListScreen';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
import RequestScreen from '../../../components/Screens/RequestScreen';
-import ConnectionManager from '../../../managers/ConnectionManager';
+import { useFocusEffect } from '@react-navigation/core';
+import { useCallback } from 'react';
+import { useNavigation } from '@react-navigation/native';
+import { useAuthenticatedRequest } from '../../../context/loginContext';
-type PropsType = {
- navigation: StackNavigationProp;
+type Props = {
route: {
params?: {
data?: ClubType;
@@ -46,7 +47,6 @@ type PropsType = {
clubId?: number;
};
};
- theme: ReactNativePaper.Theme;
};
type ResponseType = ClubType;
@@ -89,33 +89,28 @@ const styles = StyleSheet.create({
* If called with data and categories navigation parameters, will use those to display the data.
* If called with clubId parameter, will fetch the information on the server
*/
-class ClubDisplayScreen extends React.Component {
- displayData: ClubType | undefined;
+function ClubDisplayScreen(props: Props) {
+ const navigation = useNavigation();
+ const theme = useTheme();
- categories: Array | null;
+ const [displayData, setDisplayData] = useState();
+ const [categories, setCategories] = useState<
+ Array | undefined
+ >();
+ const [clubId, setClubId] = useState();
- clubId: number;
-
- shouldFetchData: boolean;
-
- constructor(props: PropsType) {
- super(props);
- this.displayData = undefined;
- this.categories = null;
- this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
- this.shouldFetchData = true;
-
- if (
- props.route.params &&
- props.route.params.data &&
- props.route.params.categories
- ) {
- this.displayData = props.route.params.data;
- this.categories = props.route.params.categories;
- this.clubId = props.route.params.data.id;
- this.shouldFetchData = false;
- }
- }
+ useFocusEffect(
+ useCallback(() => {
+ if (props.route.params?.data && props.route.params?.categories) {
+ setDisplayData(props.route.params.data);
+ setCategories(props.route.params.categories);
+ setClubId(props.route.params.data.id);
+ } else {
+ const id = props.route.params?.clubId;
+ setClubId(id ? id : 0);
+ }
+ }, [props.route.params])
+ );
/**
* Gets the name of the category with the given ID
@@ -123,17 +118,17 @@ class ClubDisplayScreen extends React.Component {
* @param id The category's ID
* @returns {string|*}
*/
- getCategoryName(id: number): string {
+ const getCategoryName = (id: number): string => {
let categoryName = '';
- if (this.categories !== null) {
- this.categories.forEach((item: ClubCategoryType) => {
+ if (categories) {
+ categories.forEach((item: ClubCategoryType) => {
if (id === item.id) {
categoryName = item.name;
}
});
}
return categoryName;
- }
+ };
/**
* Gets the view for rendering categories
@@ -141,23 +136,23 @@ class ClubDisplayScreen extends React.Component {
* @param categories The categories to display (max 2)
* @returns {null|*}
*/
- getCategoriesRender(categories: Array) {
- if (this.categories == null) {
+ const getCategoriesRender = (c: Array) => {
+ if (!categories) {
return null;
}
const final: Array = [];
- categories.forEach((cat: number | null) => {
+ c.forEach((cat: number | null) => {
if (cat != null) {
final.push(
- {this.getCategoryName(cat)}
+ {getCategoryName(cat)}
);
}
});
return {final};
- }
+ };
/**
* Gets the view for rendering club managers if any
@@ -166,8 +161,7 @@ class ClubDisplayScreen extends React.Component {
* @param email The club contact email
* @returns {*}
*/
- getManagersRender(managers: Array, email: string | null) {
- const { props } = this;
+ const getManagersRender = (managers: Array, email: string | null) => {
const managersListView: Array = [];
managers.forEach((item: string) => {
managersListView.push({item});
@@ -191,22 +185,18 @@ class ClubDisplayScreen extends React.Component {
)}
/>
{managersListView}
- {ClubDisplayScreen.getEmailButton(email, hasManagers)}
+ {getEmailButton(email, hasManagers)}
);
- }
+ };
/**
* Gets the email button to contact the club, or the amicale if the club does not have any managers
@@ -215,7 +205,7 @@ class ClubDisplayScreen extends React.Component {
* @param hasManagers True if the club has managers
* @returns {*}
*/
- static getEmailButton(email: string | null, hasManagers: boolean) {
+ const getEmailButton = (email: string | null, hasManagers: boolean) => {
const destinationEmail =
email != null && hasManagers ? email : AMICALE_MAIL;
const text =
@@ -236,14 +226,14 @@ class ClubDisplayScreen extends React.Component {
);
- }
+ };
- getScreen = (data: ResponseType | undefined) => {
+ const getScreen = (data: ResponseType | undefined) => {
if (data) {
- this.updateHeaderTitle(data);
+ updateHeaderTitle(data);
return (
- {this.getCategoriesRender(data.category)}
+ {getCategoriesRender(data.category)}
{data.logo !== null ? (
{
) : (
)}
- {this.getManagersRender(data.responsibles, data.email)}
+ {getManagersRender(data.responsibles, data.email)}
);
}
@@ -273,27 +263,22 @@ class ClubDisplayScreen extends React.Component {
*
* @param data The club data
*/
- updateHeaderTitle(data: ClubType) {
- const { props } = this;
- props.navigation.setOptions({ title: data.name });
- }
+ const updateHeaderTitle = (data: ClubType) => {
+ navigation.setOptions({ title: data.name });
+ };
- render() {
- if (this.shouldFetchData) {
- return (
-
- ConnectionManager.getInstance().authenticatedRequest(
- 'clubs/info',
- { id: this.clubId }
- )
- }
- render={this.getScreen}
- />
- );
- }
- return this.getScreen(this.displayData);
- }
+ const request = useAuthenticatedRequest('clubs/info', {
+ id: clubId,
+ });
+
+ return (
+
+ );
}
-export default withTheme(ClubDisplayScreen);
+export default ClubDisplayScreen;
diff --git a/src/screens/Amicale/Clubs/ClubListScreen.tsx b/src/screens/Amicale/Clubs/ClubListScreen.tsx
index c86ca82..048c343 100644
--- a/src/screens/Amicale/Clubs/ClubListScreen.tsx
+++ b/src/screens/Amicale/Clubs/ClubListScreen.tsx
@@ -17,11 +17,10 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
+import React, { useLayoutEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';
import { Searchbar } from 'react-native-paper';
import i18n from 'i18n-js';
-import { StackNavigationProp } from '@react-navigation/stack';
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
import {
isItemInCategoryFilter,
@@ -31,8 +30,9 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
import MaterialHeaderButtons, {
Item,
} from '../../../components/Overrides/CustomHeaderButton';
-import ConnectionManager from '../../../managers/ConnectionManager';
import WebSectionList from '../../../components/Screens/WebSectionList';
+import { useNavigation } from '@react-navigation/native';
+import { useAuthenticatedRequest } from '../../../context/loginContext';
export type ClubCategoryType = {
id: number;
@@ -49,15 +49,6 @@ export type ClubType = {
responsibles: Array;
};
-type PropsType = {
- navigation: StackNavigationProp;
-};
-
-type StateType = {
- currentlySelectedCategories: Array;
- currentSearchString: string;
-};
-
type ResponseType = {
categories: Array;
clubs: Array;
@@ -65,33 +56,52 @@ type ResponseType = {
const LIST_ITEM_HEIGHT = 96;
-class ClubListScreen extends React.Component {
- categories: Array;
+function ClubListScreen() {
+ const navigation = useNavigation();
+ const request = useAuthenticatedRequest('clubs/list');
+ const [
+ currentlySelectedCategories,
+ setCurrentlySelectedCategories,
+ ] = useState>([]);
+ const [currentSearchString, setCurrentSearchString] = useState('');
+ const categories = useRef>([]);
- constructor(props: PropsType) {
- super(props);
- this.categories = [];
- this.state = {
- currentlySelectedCategories: [],
- currentSearchString: '',
+ useLayoutEffect(() => {
+ const getSearchBar = () => {
+ return (
+ // @ts-ignore
+
+ );
};
- }
-
- /**
- * Creates the header content
- */
- componentDidMount() {
- const { props } = this;
- props.navigation.setOptions({
- headerTitle: this.getSearchBar,
- headerRight: this.getHeaderButtons,
+ const getHeaderButtons = () => {
+ return (
+
+ - navigation.navigate('club-about')}
+ />
+
+ );
+ };
+ navigation.setOptions({
+ headerTitle: getSearchBar,
+ headerRight: getHeaderButtons,
headerBackTitleVisible: false,
headerTitleContainerStyle:
Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 },
});
- }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [navigation]);
+
+ const onSearchStringChange = (str: string) => {
+ updateFilteredData(str, null);
+ };
/**
* Callback used when clicking an article in the list.
@@ -99,61 +109,20 @@ class ClubListScreen extends React.Component {
*
* @param item The article pressed
*/
- onListItemPress(item: ClubType) {
- const { props } = this;
- props.navigation.navigate('club-information', {
+ const onListItemPress = (item: ClubType) => {
+ navigation.navigate('club-information', {
data: item,
- categories: this.categories,
+ categories: categories.current,
});
- }
-
- /**
- * Callback used when the search changes
- *
- * @param str The new search string
- */
- onSearchStringChange = (str: string) => {
- this.updateFilteredData(str, null);
};
- /**
- * Gets the header search bar
- *
- * @return {*}
- */
- getSearchBar = () => {
- return (
- // @ts-ignore
-
- );
+ const onChipSelect = (id: number) => {
+ updateFilteredData(null, id);
};
- onChipSelect = (id: number) => {
- this.updateFilteredData(null, id);
- };
-
- /**
- * Gets the header button
- * @return {*}
- */
- getHeaderButtons = () => {
- const onPress = () => {
- const { props } = this;
- props.navigation.navigate('club-about');
- };
- return (
-
-
-
- );
- };
-
- createDataset = (data: ResponseType | undefined) => {
+ const createDataset = (data: ResponseType | undefined) => {
if (data) {
- this.categories = data?.categories;
+ categories.current = data.categories;
return [{ title: '', data: data.clubs }];
} else {
return [];
@@ -165,30 +134,23 @@ class ClubListScreen extends React.Component {
*
* @returns {*}
*/
- getListHeader(data: ResponseType | undefined) {
- const { state } = this;
+ const getListHeader = (data: ResponseType | undefined) => {
if (data) {
return (
);
} else {
return null;
}
- }
+ };
- /**
- * Gets the category object of the given ID
- *
- * @param id The ID of the category to find
- * @returns {*}
- */
- getCategoryOfId = (id: number): ClubCategoryType | null => {
+ const getCategoryOfId = (id: number): ClubCategoryType | null => {
let cat = null;
- this.categories.forEach((item: ClubCategoryType) => {
+ categories.current.forEach((item: ClubCategoryType) => {
if (id === item.id) {
cat = item;
}
@@ -196,14 +158,14 @@ class ClubListScreen extends React.Component {
return cat;
};
- getRenderItem = ({ item }: { item: ClubType }) => {
+ const getRenderItem = ({ item }: { item: ClubType }) => {
const onPress = () => {
- this.onListItemPress(item);
+ onListItemPress(item);
};
- if (this.shouldRenderItem(item)) {
+ if (shouldRenderItem(item)) {
return (
{
return null;
};
- keyExtractor = (item: ClubType): string => item.id.toString();
+ const keyExtractor = (item: ClubType): string => item.id.toString();
/**
* Updates the search string and category filter, saving them to the State.
@@ -224,10 +186,12 @@ class ClubListScreen extends React.Component {
* @param filterStr The new filter string to use
* @param categoryId The category to add/remove from the filter
*/
- updateFilteredData(filterStr: string | null, categoryId: number | null) {
- const { state } = this;
- const newCategoriesState = [...state.currentlySelectedCategories];
- let newStrState = state.currentSearchString;
+ const updateFilteredData = (
+ filterStr: string | null,
+ categoryId: number | null
+ ) => {
+ const newCategoriesState = [...currentlySelectedCategories];
+ let newStrState = currentSearchString;
if (filterStr !== null) {
newStrState = filterStr;
}
@@ -240,12 +204,10 @@ class ClubListScreen extends React.Component {
}
}
if (filterStr !== null || categoryId !== null) {
- this.setState({
- currentSearchString: newStrState,
- currentlySelectedCategories: newCategoriesState,
- });
+ setCurrentSearchString(newStrState);
+ setCurrentlySelectedCategories(newCategoriesState);
}
- }
+ };
/**
* Checks if the given item should be rendered according to current name and category filters
@@ -253,35 +215,28 @@ class ClubListScreen extends React.Component {
* @param item The club to check
* @returns {boolean}
*/
- shouldRenderItem(item: ClubType): boolean {
- const { state } = this;
+ const shouldRenderItem = (item: ClubType): boolean => {
let shouldRender =
- state.currentlySelectedCategories.length === 0 ||
- isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
+ currentlySelectedCategories.length === 0 ||
+ isItemInCategoryFilter(currentlySelectedCategories, item.category);
if (shouldRender) {
- shouldRender = stringMatchQuery(item.name, state.currentSearchString);
+ shouldRender = stringMatchQuery(item.name, currentSearchString);
}
return shouldRender;
- }
+ };
- render() {
- return (
-
- ConnectionManager.getInstance().authenticatedRequest(
- 'clubs/list'
- )
- }
- createDataset={this.createDataset}
- keyExtractor={this.keyExtractor}
- renderItem={this.getRenderItem}
- renderListHeaderComponent={(data) => this.getListHeader(data)}
- // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
- removeClippedSubviews={true}
- itemHeight={LIST_ITEM_HEIGHT}
- />
- );
- }
+ return (
+
+ );
}
export default ClubListScreen;
diff --git a/src/screens/Amicale/Equipment/EquipmentListScreen.tsx b/src/screens/Amicale/Equipment/EquipmentListScreen.tsx
index 8dfe8af..f437168 100644
--- a/src/screens/Amicale/Equipment/EquipmentListScreen.tsx
+++ b/src/screens/Amicale/Equipment/EquipmentListScreen.tsx
@@ -17,26 +17,17 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
+import React, { useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Button } from 'react-native-paper';
-import { StackNavigationProp } from '@react-navigation/stack';
import i18n from 'i18n-js';
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import MascotPopup from '../../../components/Mascot/MascotPopup';
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import GENERAL_STYLES from '../../../constants/Styles';
-import ConnectionManager from '../../../managers/ConnectionManager';
import { ApiRejectType } from '../../../utils/WebData';
import WebSectionList from '../../../components/Screens/WebSectionList';
-
-type PropsType = {
- navigation: StackNavigationProp;
-};
-
-type StateType = {
- mascotDialogVisible: boolean | undefined;
-};
+import { useAuthenticatedRequest } from '../../../context/loginContext';
export type DeviceType = {
id: number;
@@ -67,69 +58,62 @@ const styles = StyleSheet.create({
},
});
-class EquipmentListScreen extends React.Component {
- userRents: null | Array;
+function EquipmentListScreen() {
+ const userRents = useRef>();
+ const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
- constructor(props: PropsType) {
- super(props);
- this.userRents = null;
- this.state = {
- mascotDialogVisible: undefined,
- };
- }
+ const requestAll = useAuthenticatedRequest<{ devices: Array }>(
+ 'location/all'
+ );
+ const requestOwn = useAuthenticatedRequest<{
+ locations: Array;
+ }>('location/my');
- getRenderItem = ({ item }: { item: DeviceType }) => {
- const { navigation } = this.props;
+ const getRenderItem = ({ item }: { item: DeviceType }) => {
return (
);
};
- getUserDeviceRentDates(item: DeviceType): [string, string] | null {
+ const getUserDeviceRentDates = (
+ item: DeviceType
+ ): [string, string] | null => {
let dates = null;
- if (this.userRents != null) {
- this.userRents.forEach((device: RentedDeviceType) => {
+ if (userRents.current) {
+ userRents.current.forEach((device: RentedDeviceType) => {
if (item.id === device.device_id) {
dates = [device.begin, device.end];
}
});
}
return dates;
- }
+ };
- /**
- * Gets the list header, with explains this screen's purpose
- *
- * @returns {*}
- */
- getListHeader() {
+ const getListHeader = () => {
return (
);
- }
+ };
- keyExtractor = (item: DeviceType): string => item.id.toString();
+ const keyExtractor = (item: DeviceType): string => item.id.toString();
- createDataset = (data: ResponseType | undefined) => {
+ const createDataset = (data: ResponseType | undefined) => {
if (data) {
- const userRents = data.locations;
-
- if (userRents) {
- this.userRents = userRents;
+ if (data.locations) {
+ userRents.current = data.locations;
}
return [{ title: '', data: data.devices }];
} else {
@@ -137,27 +121,19 @@ class EquipmentListScreen extends React.Component {
}
};
- showMascotDialog = () => {
- this.setState({ mascotDialogVisible: true });
- };
+ const showMascotDialog = () => setMascotDialogVisible(true);
- hideMascotDialog = () => {
- this.setState({ mascotDialogVisible: false });
- };
+ const hideMascotDialog = () => setMascotDialogVisible(false);
- request = () => {
+ const request = () => {
return new Promise(
(
resolve: (data: ResponseType) => void,
reject: (error: ApiRejectType) => void
) => {
- ConnectionManager.getInstance()
- .authenticatedRequest<{ devices: Array }>('location/all')
+ requestAll()
.then((devicesData) => {
- ConnectionManager.getInstance()
- .authenticatedRequest<{
- locations: Array;
- }>('location/my')
+ requestOwn()
.then((rentsData) => {
resolve({
devices: devicesData.devices,
@@ -175,34 +151,31 @@ class EquipmentListScreen extends React.Component {
);
};
- render() {
- const { state } = this;
- return (
-
- this.getListHeader()}
- />
-
-
- );
- }
+ return (
+
+
+
+
+ );
}
export default EquipmentListScreen;
diff --git a/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx b/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx
index e81fbb8..a145c98 100644
--- a/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx
+++ b/src/screens/Amicale/Equipment/EquipmentRentScreen.tsx
@@ -17,21 +17,20 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
+import React, { useCallback, useRef, useState } from 'react';
import {
Button,
Caption,
Card,
Headline,
Subheading,
- withTheme,
+ useTheme,
} from 'react-native-paper';
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
import { BackHandler, StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js';
import { CalendarList, PeriodMarking } from 'react-native-calendars';
-import type { DeviceType } from './EquipmentListScreen';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import {
@@ -42,34 +41,21 @@ import {
getValidRange,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
-import ConnectionManager from '../../../managers/ConnectionManager';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import { MainStackParamsList } from '../../../navigation/MainNavigator';
import GENERAL_STYLES from '../../../constants/Styles';
import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests';
+import { useFocusEffect } from '@react-navigation/core';
+import { useNavigation } from '@react-navigation/native';
+import { useAuthenticatedRequest } from '../../../context/loginContext';
-type EquipmentRentScreenNavigationProp = StackScreenProps<
- MainStackParamsList,
- 'equipment-rent'
->;
-
-type Props = EquipmentRentScreenNavigationProp & {
- navigation: StackNavigationProp;
- theme: ReactNativePaper.Theme;
-};
+type Props = StackScreenProps;
export type MarkedDatesObjectType = {
[key: string]: PeriodMarking;
};
-type StateType = {
- dialogVisible: boolean;
- errorDialogVisible: boolean;
- markedDates: MarkedDatesObjectType;
- currentError: ApiRejectType;
-};
-
const styles = StyleSheet.create({
titleContainer: {
marginLeft: 'auto',
@@ -114,98 +100,101 @@ const styles = StyleSheet.create({
},
});
-class EquipmentRentScreen extends React.Component {
- item: DeviceType | null;
+function EquipmentRentScreen(props: Props) {
+ const theme = useTheme();
+ const navigation = useNavigation>();
+ const [currentError, setCurrentError] = useState({
+ status: REQUEST_STATUS.SUCCESS,
+ });
+ const [markedDates, setMarkedDates] = useState({});
+ const [dialogVisible, setDialogVisible] = useState(false);
- bookedDates: Array;
+ const item = props.route.params.item;
- bookRef: { current: null | (Animatable.View & View) };
+ const bookedDates = useRef>([]);
+ const canBookEquipment = useRef(false);
- canBookEquipment: boolean;
+ const bookRef = useRef(null);
- lockedDates: {
+ let lockedDates: {
[key: string]: PeriodMarking;
- };
+ } = {};
- constructor(props: Props) {
- super(props);
- this.item = null;
- this.lockedDates = {};
- this.state = {
- dialogVisible: false,
- errorDialogVisible: false,
- markedDates: {},
- currentError: { status: REQUEST_STATUS.SUCCESS },
- };
- this.resetSelection();
- this.bookRef = React.createRef();
- this.canBookEquipment = false;
- this.bookedDates = [];
- if (props.route.params != null) {
- if (props.route.params.item != null) {
- this.item = props.route.params.item;
- } else {
- this.item = null;
- }
- }
- const { item } = this;
- if (item != null) {
- this.lockedDates = {};
- item.booked_at.forEach((date: { begin: string; end: string }) => {
- const range = getValidRange(
- new Date(date.begin),
- new Date(date.end),
- null
- );
- this.lockedDates = {
- ...this.lockedDates,
- ...generateMarkedDates(false, props.theme, range),
- };
- });
- }
+ if (item) {
+ item.booked_at.forEach((date: { begin: string; end: string }) => {
+ const range = getValidRange(
+ new Date(date.begin),
+ new Date(date.end),
+ null
+ );
+ lockedDates = {
+ ...lockedDates,
+ ...generateMarkedDates(false, theme, range),
+ };
+ });
}
- /**
- * Captures focus and blur events to hook on android back button
- */
- componentDidMount() {
- const { navigation } = this.props;
- navigation.addListener('focus', () => {
+ useFocusEffect(
+ useCallback(() => {
BackHandler.addEventListener(
'hardwareBackPress',
- this.onBackButtonPressAndroid
+ onBackButtonPressAndroid
);
- });
- navigation.addListener('blur', () => {
- BackHandler.removeEventListener(
- 'hardwareBackPress',
- this.onBackButtonPressAndroid
- );
- });
- }
+ return () => {
+ BackHandler.removeEventListener(
+ 'hardwareBackPress',
+ onBackButtonPressAndroid
+ );
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+ );
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
- onBackButtonPressAndroid = (): boolean => {
- if (this.bookedDates.length > 0) {
- this.resetSelection();
- this.updateMarkedSelection();
+ const onBackButtonPressAndroid = (): boolean => {
+ if (bookedDates.current.length > 0) {
+ resetSelection();
+ updateMarkedSelection();
return true;
}
return false;
};
- onDialogDismiss = () => {
- this.setState({ dialogVisible: false });
+ const showDialog = () => setDialogVisible(true);
+
+ const onDialogDismiss = () => setDialogVisible(false);
+
+ const onErrorDialogDismiss = () =>
+ setCurrentError({ status: REQUEST_STATUS.SUCCESS });
+
+ const getBookStartDate = (): Date | null => {
+ return bookedDates.current.length > 0
+ ? new Date(bookedDates.current[0])
+ : null;
};
- onErrorDialogDismiss = () => {
- this.setState({ errorDialogVisible: false });
+ const getBookEndDate = (): Date | null => {
+ const { length } = bookedDates.current;
+ return length > 0 ? new Date(bookedDates.current[length - 1]) : null;
};
+ const start = getBookStartDate();
+ const end = getBookEndDate();
+ const request = useAuthenticatedRequest(
+ 'location/booking',
+ item && start && end
+ ? {
+ device: item.id,
+ begin: getISODate(start),
+ end: getISODate(end),
+ }
+ : undefined
+ );
+
/**
* Sends the selected data to the server and waits for a response.
* If the request is a success, navigate to the recap screen.
@@ -213,54 +202,37 @@ class EquipmentRentScreen extends React.Component {
*
* @returns {Promise}
*/
- onDialogAccept = (): Promise => {
+ const onDialogAccept = (): Promise => {
return new Promise((resolve: () => void) => {
- const { item, props } = this;
- const start = this.getBookStartDate();
- const end = this.getBookEndDate();
if (item != null && start != null && end != null) {
- ConnectionManager.getInstance()
- .authenticatedRequest('location/booking', {
- device: item.id,
- begin: getISODate(start),
- end: getISODate(end),
- })
+ request()
.then(() => {
- this.onDialogDismiss();
- props.navigation.replace('equipment-confirm', {
- item: this.item,
+ onDialogDismiss();
+ navigation.replace('equipment-confirm', {
+ item: item,
dates: [getISODate(start), getISODate(end)],
});
resolve();
})
.catch((error: ApiRejectType) => {
- this.onDialogDismiss();
- this.showErrorDialog(error);
+ onDialogDismiss();
+ setCurrentError(error);
resolve();
});
} else {
- this.onDialogDismiss();
+ onDialogDismiss();
resolve();
}
});
};
- getBookStartDate(): Date | null {
- return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
- }
-
- getBookEndDate(): Date | null {
- const { length } = this.bookedDates;
- return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
- }
-
/**
* Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all.
*
* @param day The day selected
*/
- selectNewDate = (day: {
+ const selectNewDate = (day: {
dateString: string;
day: number;
month: number;
@@ -268,222 +240,196 @@ class EquipmentRentScreen extends React.Component {
year: number;
}) => {
const selected = new Date(day.dateString);
- const start = this.getBookStartDate();
- if (!this.lockedDates[day.dateString] != null) {
+ if (!lockedDates[day.dateString] != null) {
if (start === null) {
- this.updateSelectionRange(selected, selected);
- this.enableBooking();
+ updateSelectionRange(selected, selected);
+ enableBooking();
} else if (start.getTime() === selected.getTime()) {
- this.resetSelection();
- } else if (this.bookedDates.length === 1) {
- this.updateSelectionRange(start, selected);
- this.enableBooking();
+ resetSelection();
+ } else if (bookedDates.current.length === 1) {
+ updateSelectionRange(start, selected);
+ enableBooking();
} else {
- this.resetSelection();
+ resetSelection();
}
- this.updateMarkedSelection();
+ updateMarkedSelection();
}
};
- showErrorDialog = (error: ApiRejectType) => {
- this.setState({
- errorDialogVisible: true,
- currentError: error,
- });
+ const showBookButton = () => {
+ if (bookRef.current && bookRef.current.fadeInUp) {
+ bookRef.current.fadeInUp(500);
+ }
};
- showDialog = () => {
- this.setState({ dialogVisible: true });
+ const hideBookButton = () => {
+ if (bookRef.current && bookRef.current.fadeOutDown) {
+ bookRef.current.fadeOutDown(500);
+ }
};
- /**
- * Shows the book button by plying a fade animation
- */
- showBookButton() {
- if (this.bookRef.current && this.bookRef.current.fadeInUp) {
- this.bookRef.current.fadeInUp(500);
+ const enableBooking = () => {
+ if (!canBookEquipment.current) {
+ showBookButton();
+ canBookEquipment.current = true;
}
- }
+ };
- /**
- * Hides the book button by plying a fade animation
- */
- hideBookButton() {
- if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
- this.bookRef.current.fadeOutDown(500);
+ const resetSelection = () => {
+ if (canBookEquipment.current) {
+ hideBookButton();
}
- }
+ canBookEquipment.current = false;
+ bookedDates.current = [];
+ };
- enableBooking() {
- if (!this.canBookEquipment) {
- this.showBookButton();
- this.canBookEquipment = true;
- }
- }
-
- resetSelection() {
- if (this.canBookEquipment) {
- this.hideBookButton();
- }
- this.canBookEquipment = false;
- this.bookedDates = [];
- }
-
- updateSelectionRange(start: Date, end: Date) {
- this.bookedDates = getValidRange(start, end, this.item);
- }
-
- updateMarkedSelection() {
- const { theme } = this.props;
- this.setState({
- markedDates: generateMarkedDates(true, theme, this.bookedDates),
- });
- }
-
- render() {
- const { item, props, state } = this;
- const start = this.getBookStartDate();
- const end = this.getBookEndDate();
- let subHeadingText;
- if (start == null) {
- subHeadingText = i18n.t('screens.equipment.booking');
- } else if (end != null && start.getTime() !== end.getTime()) {
- subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
- begin: getRelativeDateString(start),
- end: getRelativeDateString(end),
- });
+ const updateSelectionRange = (s: Date, e: Date) => {
+ if (item) {
+ bookedDates.current = getValidRange(s, e, item);
} else {
- subHeadingText = i18n.t('screens.equipment.bookingDay', {
- date: getRelativeDateString(start),
- });
+ bookedDates.current = [];
}
- if (item != null) {
- const isAvailable = isEquipmentAvailable(item);
- const firstAvailability = getFirstEquipmentAvailability(item);
- return (
-
-
-
-
-
-
- {item.name}
-
- (
- {i18n.t('screens.equipment.bail', { cost: item.caution })}
- )
-
-
-
+ };
-
-
- {subHeadingText}
-
-
-
-
-
-
+ const updateMarkedSelection = () => {
+ setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
+ };
-
-
-
-
-
- );
- }
- return null;
+ let subHeadingText;
+
+ if (start == null) {
+ subHeadingText = i18n.t('screens.equipment.booking');
+ } else if (end != null && start.getTime() !== end.getTime()) {
+ subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
+ begin: getRelativeDateString(start),
+ end: getRelativeDateString(end),
+ });
+ } else {
+ subHeadingText = i18n.t('screens.equipment.bookingDay', {
+ date: getRelativeDateString(start),
+ });
}
+
+ if (item) {
+ const isAvailable = isEquipmentAvailable(item);
+ const firstAvailability = getFirstEquipmentAvailability(item);
+ return (
+
+
+
+
+
+
+ {item.name}
+
+ ({i18n.t('screens.equipment.bail', { cost: item.caution })})
+
+
+
+
+
+ {subHeadingText}
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ return null;
}
-export default withTheme(EquipmentRentScreen);
+export default EquipmentRentScreen;
diff --git a/src/screens/Amicale/LoginScreen.tsx b/src/screens/Amicale/LoginScreen.tsx
index 8bf3185..cfcc7c8 100644
--- a/src/screens/Amicale/LoginScreen.tsx
+++ b/src/screens/Amicale/LoginScreen.tsx
@@ -17,19 +17,11 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
-import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
-import {
- Button,
- Card,
- HelperText,
- TextInput,
- withTheme,
-} from 'react-native-paper';
+import React, { useCallback, useState } from 'react';
+import { KeyboardAvoidingView, View } from 'react-native';
import i18n from 'i18n-js';
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
import LinearGradient from 'react-native-linear-gradient';
-import ConnectionManager from '../../managers/ConnectionManager';
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
@@ -37,99 +29,32 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol
import { MainStackParamsList } from '../../navigation/MainNavigator';
import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls';
-import { ApiRejectType } from '../../utils/WebData';
+import { ApiRejectType, connectToAmicale } from '../../utils/WebData';
import { REQUEST_STATUS } from '../../utils/Requests';
+import LoginForm from '../../components/Amicale/Login/LoginForm';
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
+import { TabRoutes } from '../../navigation/TabNavigator';
+import { useShouldShowMascot } from '../../context/preferencesContext';
-type LoginScreenNavigationProp = StackScreenProps;
+type Props = StackScreenProps;
-type Props = LoginScreenNavigationProp & {
- navigation: StackNavigationProp;
- theme: ReactNativePaper.Theme;
-};
+function LoginScreen(props: Props) {
+ const navigation = useNavigation>();
+ const [loading, setLoading] = useState(false);
+ const [nextScreen, setNextScreen] = useState(undefined);
+ const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
+ const [currentError, setCurrentError] = useState({
+ status: REQUEST_STATUS.SUCCESS,
+ });
+ const homeMascot = useShouldShowMascot(TabRoutes.Home);
-type StateType = {
- email: string;
- password: string;
- isEmailValidated: boolean;
- isPasswordValidated: boolean;
- loading: boolean;
- dialogVisible: boolean;
- dialogError: ApiRejectType;
- mascotDialogVisible: boolean | undefined;
-};
+ useFocusEffect(
+ useCallback(() => {
+ setNextScreen(props.route.params?.nextScreen);
+ }, [props.route.params])
+ );
-const ICON_AMICALE = require('../../../assets/amicale.png');
-
-const emailRegex = /^.+@.+\..+$/;
-
-const styles = StyleSheet.create({
- card: {
- marginTop: 'auto',
- marginBottom: 'auto',
- },
- header: {
- fontSize: 36,
- marginBottom: 48,
- },
- text: {
- color: '#ffffff',
- },
- buttonContainer: {
- flexWrap: 'wrap',
- },
- lockButton: {
- marginRight: 'auto',
- marginBottom: 20,
- },
- sendButton: {
- marginLeft: 'auto',
- },
-});
-
-class LoginScreen extends React.Component {
- onEmailChange: (value: string) => void;
-
- onPasswordChange: (value: string) => void;
-
- passwordInputRef: {
- // @ts-ignore
- current: null | TextInput;
- };
-
- nextScreen: string | null;
-
- constructor(props: Props) {
- super(props);
- this.nextScreen = null;
- this.passwordInputRef = React.createRef();
- this.onEmailChange = (value: string) => {
- this.onInputChange(true, value);
- };
- this.onPasswordChange = (value: string) => {
- this.onInputChange(false, value);
- };
- props.navigation.addListener('focus', this.onScreenFocus);
- this.state = {
- email: '',
- password: '',
- isEmailValidated: false,
- isPasswordValidated: false,
- loading: false,
- dialogVisible: false,
- dialogError: { status: REQUEST_STATUS.SUCCESS },
- mascotDialogVisible: undefined,
- };
- }
-
- onScreenFocus = () => {
- this.handleNavigationParams();
- };
-
- /**
- * Navigates to the Amicale website screen with the reset password link as navigation parameters
- */
- onResetPasswordClick = () => {
- const { navigation } = this.props;
+ const onResetPasswordClick = () => {
navigation.navigate('website', {
host: Urls.websites.amicale,
path: Urls.amicale.resetPassword,
@@ -137,38 +62,6 @@ class LoginScreen extends React.Component {
});
};
- /**
- * Called when the user input changes in the email or password field.
- * This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
- *
- * @param isEmail True if the field is the email field
- * @param value The new field value
- */
- onInputChange(isEmail: boolean, value: string) {
- if (isEmail) {
- this.setState({
- email: value,
- isEmailValidated: false,
- });
- } else {
- this.setState({
- password: value,
- isPasswordValidated: false,
- });
- }
- }
-
- /**
- * Focuses the password field when the email field is done
- *
- * @returns {*}
- */
- onEmailSubmit = () => {
- if (this.passwordInputRef.current != null) {
- this.passwordInputRef.current.focus();
- }
- };
-
/**
* Called when the user clicks on login or finishes to type his password.
*
@@ -176,294 +69,84 @@ class LoginScreen extends React.Component {
* then makes the login request and enters a loading state until the request finishes
*
*/
- onSubmit = () => {
- const { email, password } = this.state;
- if (this.shouldEnableLogin()) {
- this.setState({ loading: true });
- ConnectionManager.getInstance()
- .connect(email, password)
- .then(this.handleSuccess)
- .catch(this.showErrorDialog)
- .finally(() => {
- this.setState({ loading: false });
- });
- }
+ const onSubmit = (email: string, password: string) => {
+ setLoading(true);
+ connectToAmicale(email, password)
+ .then(handleSuccess)
+ .catch(setCurrentError)
+ .finally(() => setLoading(false));
};
- /**
- * Gets the form input
- *
- * @returns {*}
- */
- getFormInput() {
- const { email, password } = this.state;
- return (
-
-
-
- {i18n.t('screens.login.emailError')}
-
-
-
- {i18n.t('screens.login.passwordError')}
-
-
- );
- }
+ const hideMascotDialog = () => setMascotDialogVisible(true);
- /**
- * Gets the card containing the input form
- * @returns {*}
- */
- getMainCard() {
- const { props, state } = this;
- return (
-
- (
-
- )}
- />
-
- {this.getFormInput()}
-
-
-
-
-
-
-
-
-
- );
- }
+ const showMascotDialog = () => setMascotDialogVisible(false);
- /**
- * The user has unfocused the input, his email is ready to be validated
- */
- validateEmail = () => {
- this.setState({ isEmailValidated: true });
- };
-
- /**
- * The user has unfocused the input, his password is ready to be validated
- */
- validatePassword = () => {
- this.setState({ isPasswordValidated: true });
- };
-
- hideMascotDialog = () => {
- this.setState({ mascotDialogVisible: false });
- };
-
- showMascotDialog = () => {
- this.setState({ mascotDialogVisible: true });
- };
-
- /**
- * Shows an error dialog with the corresponding login error
- *
- * @param error The error given by the login request
- */
- showErrorDialog = (error: ApiRejectType) => {
- console.log(error);
-
- this.setState({
- dialogVisible: true,
- dialogError: error,
- });
- };
-
- hideErrorDialog = () => {
- this.setState({ dialogVisible: false });
- };
+ const hideErrorDialog = () =>
+ setCurrentError({ status: REQUEST_STATUS.SUCCESS });
/**
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
* Saves in user preferences to not show the login banner again.
*/
- handleSuccess = () => {
- const { navigation } = this.props;
+ const handleSuccess = () => {
// Do not show the home login banner again
- // TODO
- // AsyncStorageManager.set(
- // AsyncStorageManager.PREFERENCES.homeShowMascot.key,
- // false
- // );
- if (this.nextScreen == null) {
+ if (homeMascot.shouldShow) {
+ homeMascot.setShouldShow(false);
+ }
+ if (!nextScreen) {
navigation.goBack();
} else {
- navigation.replace(this.nextScreen);
+ navigation.replace(nextScreen);
}
};
- /**
- * Saves the screen to navigate to after a successful login if one was provided in navigation parameters
- */
- handleNavigationParams() {
- this.nextScreen = this.props.route.params?.nextScreen;
- }
-
- /**
- * Checks if the entered email is valid (matches the regex)
- *
- * @returns {boolean}
- */
- isEmailValid(): boolean {
- const { email } = this.state;
- return emailRegex.test(email);
- }
-
- /**
- * Checks if we should tell the user his email is invalid.
- * We should only show this if his email is invalid and has been checked when un-focusing the input
- *
- * @returns {boolean|boolean}
- */
- shouldShowEmailError(): boolean {
- const { isEmailValidated } = this.state;
- return isEmailValidated && !this.isEmailValid();
- }
-
- /**
- * Checks if the user has entered a password
- *
- * @returns {boolean}
- */
- isPasswordValid(): boolean {
- const { password } = this.state;
- return password !== '';
- }
-
- /**
- * Checks if we should tell the user his password is invalid.
- * We should only show this if his password is invalid and has been checked when un-focusing the input
- *
- * @returns {boolean|boolean}
- */
- shouldShowPasswordError(): boolean {
- const { isPasswordValidated } = this.state;
- return isPasswordValidated && !this.isPasswordValid();
- }
-
- /**
- * If the email and password are valid, and we are not loading a request, then the login button can be enabled
- *
- * @returns {boolean}
- */
- shouldEnableLogin(): boolean {
- const { loading } = this.state;
- return this.isEmailValid() && this.isPasswordValid() && !loading;
- }
-
- render() {
- const { mascotDialogVisible, dialogVisible, dialogError } = this.state;
- return (
-
+
-
-
- {this.getMainCard()}
-
+
+
-
-
-
-
- );
- }
+
+
+
+
+
+
+ );
}
-export default withTheme(LoginScreen);
+export default LoginScreen;
diff --git a/src/screens/Amicale/ProfileScreen.tsx b/src/screens/Amicale/ProfileScreen.tsx
index ed7004d..fa3a57c 100644
--- a/src/screens/Amicale/ProfileScreen.tsx
+++ b/src/screens/Amicale/ProfileScreen.tsx
@@ -17,52 +17,29 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
-import { FlatList, StyleSheet, View } from 'react-native';
-import {
- Avatar,
- Button,
- Card,
- Divider,
- List,
- Paragraph,
- withTheme,
-} from 'react-native-paper';
-import i18n from 'i18n-js';
-import { StackNavigationProp } from '@react-navigation/stack';
+import React, { useLayoutEffect, useState } from 'react';
+import { View } from 'react-native';
import LogoutDialog from '../../components/Amicale/LogoutDialog';
import MaterialHeaderButtons, {
Item,
} from '../../components/Overrides/CustomHeaderButton';
-import CardList from '../../components/Lists/CardList/CardList';
-import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import GENERAL_STYLES from '../../constants/Styles';
-import Urls from '../../constants/Urls';
import RequestScreen from '../../components/Screens/RequestScreen';
-import ConnectionManager from '../../managers/ConnectionManager';
-import {
- getAmicaleServices,
- ServiceItemType,
- SERVICES_KEY,
-} from '../../utils/Services';
+import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
+import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
+import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
+import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
+import { useNavigation } from '@react-navigation/core';
+import { useAuthenticatedRequest } from '../../context/loginContext';
-type PropsType = {
- navigation: StackNavigationProp;
- theme: ReactNativePaper.Theme;
-};
-
-type StateType = {
- dialogVisible: boolean;
-};
-
-type ClubType = {
+export type ProfileClubType = {
id: number;
name: string;
is_manager: boolean;
};
-type ProfileDataType = {
+export type ProfileDataType = {
first_name: string;
last_name: string;
email: string;
@@ -71,87 +48,68 @@ type ProfileDataType = {
branch: string;
link: string;
validity: boolean;
- clubs: Array;
+ clubs: Array;
};
-const styles = StyleSheet.create({
- card: {
- margin: 10,
- },
- icon: {
- backgroundColor: 'transparent',
- },
- editButton: {
- marginLeft: 'auto',
- },
- mascot: {
- width: 60,
- },
- title: {
- marginLeft: 10,
- },
-});
+function ProfileScreen() {
+ const navigation = useNavigation();
+ const [dialogVisible, setDialogVisible] = useState(false);
+ const request = useAuthenticatedRequest('user/profile');
-class ProfileScreen extends React.Component {
- data: ProfileDataType | undefined;
-
- flatListData: Array<{ id: string }>;
-
- amicaleDataset: Array;
-
- constructor(props: PropsType) {
- super(props);
- this.data = undefined;
- this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
- this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [
- SERVICES_KEY.PROFILE,
- ]);
- this.state = {
- dialogVisible: false,
- };
- }
-
- componentDidMount() {
- const { navigation } = this.props;
+ useLayoutEffect(() => {
+ const getHeaderButton = () => (
+
+
+
+ );
navigation.setOptions({
- headerRight: this.getHeaderButton,
+ headerRight: getHeaderButton,
});
- }
+ }, [navigation]);
- /**
- * Gets the logout header button
- *
- * @returns {*}
- */
- getHeaderButton = () => (
-
-
-
- );
-
- /**
- * Gets the main screen component with the fetched data
- *
- * @param data The data fetched from the server
- * @returns {*}
- */
- getScreen = (data: ProfileDataType | undefined) => {
- const { dialogVisible } = this.state;
+ const getScreen = (data: ProfileDataType | undefined) => {
if (data) {
- this.data = data;
+ const flatListData: Array<{
+ id: string;
+ render: () => React.ReactElement;
+ }> = [];
+ for (let i = 0; i < 4; i++) {
+ switch (i) {
+ case 0:
+ flatListData.push({
+ id: i.toString(),
+ render: () => ,
+ });
+ break;
+ case 1:
+ flatListData.push({
+ id: i.toString(),
+ render: () => ,
+ });
+ break;
+ case 2:
+ flatListData.push({
+ id: i.toString(),
+ render: () => ,
+ });
+ break;
+ default:
+ flatListData.push({
+ id: i.toString(),
+ render: () => ,
+ });
+ }
+ }
return (
-
+
);
@@ -160,346 +118,17 @@ class ProfileScreen extends React.Component {
}
};
- getRenderItem = ({ item }: { item: { id: string } }) => {
- switch (item.id) {
- case '0':
- return this.getWelcomeCard();
- case '1':
- return this.getPersonalCard();
- case '2':
- return this.getClubCard();
- default:
- return this.getMembershipCar();
- }
- };
+ const getRenderItem = ({
+ item,
+ }: {
+ item: { id: string; render: () => React.ReactElement };
+ }) => item.render();
- /**
- * Gets the list of services available with the Amicale account
- *
- * @returns {*}
- */
- getServicesList() {
- return ;
- }
+ const showDisconnectDialog = () => setDialogVisible(true);
- /**
- * Gets a card welcoming the user to his account
- *
- * @returns {*}
- */
- getWelcomeCard() {
- const { navigation } = this.props;
- return (
-
- (
-
- )}
- titleStyle={styles.title}
- />
-
-
- {i18n.t('screens.profile.welcomeDescription')}
- {this.getServicesList()}
- {i18n.t('screens.profile.welcomeFeedback')}
-
-
-
-
-
-
- );
- }
+ const hideDisconnectDialog = () => setDialogVisible(false);
- /**
- * Gets the given field value.
- * If the field does not have a value, returns a placeholder text
- *
- * @param field The field to get the value from
- * @return {*}
- */
- static getFieldValue(field?: string): string {
- return field ? field : i18n.t('screens.profile.noData');
- }
-
- /**
- * Gets a list item showing personal information
- *
- * @param field The field to display
- * @param icon The icon to use
- * @return {*}
- */
- getPersonalListItem(field: string | undefined, icon: string) {
- const { theme } = this.props;
- const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
- const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
- return (
- (
-
- )}
- />
- );
- }
-
- /**
- * Gets a card containing user personal information
- *
- * @return {*}
- */
- getPersonalCard() {
- const { theme, navigation } = this.props;
- return (
-
- (
-
- )}
- />
-
-
-
-
- {i18n.t('screens.profile.personalInformation')}
-
- {this.getPersonalListItem(this.data?.birthday, 'cake-variant')}
- {this.getPersonalListItem(this.data?.phone, 'phone')}
- {this.getPersonalListItem(this.data?.email, 'email')}
- {this.getPersonalListItem(this.data?.branch, 'school')}
-
-
-
-
-
-
-
- );
- }
-
- /**
- * Gets a cars containing clubs the user is part of
- *
- * @return {*}
- */
- getClubCard() {
- const { theme } = this.props;
- return (
-
- (
-
- )}
- />
-
-
- {this.getClubList(this.data?.clubs)}
-
-
- );
- }
-
- /**
- * Gets a card showing if the user has payed his membership
- *
- * @return {*}
- */
- getMembershipCar() {
- const { theme } = this.props;
- return (
-
- (
-
- )}
- />
-
-
- {this.getMembershipItem(this.data?.validity === true)}
-
-
-
- );
- }
-
- /**
- * Gets the item showing if the user has payed his membership
- *
- * @return {*}
- */
- getMembershipItem(state: boolean) {
- const { theme } = this.props;
- return (
- (
-
- )}
- />
- );
- }
-
- /**
- * Gets a list item for the club list
- *
- * @param item The club to render
- * @return {*}
- */
- getClubListItem = ({ item }: { item: ClubType }) => {
- const { theme } = this.props;
- const onPress = () => {
- this.openClubDetailsScreen(item.id);
- };
- let description = i18n.t('screens.profile.isMember');
- let icon = (props: {
- color: string;
- style: {
- marginLeft: number;
- marginRight: number;
- marginVertical?: number;
- };
- }) => (
-
- );
- if (item.is_manager) {
- description = i18n.t('screens.profile.isManager');
- icon = (props) => (
-
- );
- }
- return (
-
- );
- };
-
- /**
- * Renders the list of clubs the user is part of
- *
- * @param list The club list
- * @return {*}
- */
- getClubList(list: Array | undefined) {
- if (!list) {
- return null;
- }
-
- list.sort(this.sortClubList);
- return (
-
- );
- }
-
- clubKeyExtractor = (item: ClubType): string => item.name;
-
- sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
-
- showDisconnectDialog = () => {
- this.setState({ dialogVisible: true });
- };
-
- hideDisconnectDialog = () => {
- this.setState({ dialogVisible: false });
- };
-
- /**
- * Opens the club details screen for the club of given ID
- * @param id The club's id to open
- */
- openClubDetailsScreen(id: number) {
- const { navigation } = this.props;
- navigation.navigate('club-information', { clubId: id });
- }
-
- render() {
- return (
-
- request={() =>
- ConnectionManager.getInstance().authenticatedRequest('user/profile')
- }
- render={this.getScreen}
- />
- );
- }
+ return ;
}
-export default withTheme(ProfileScreen);
+export default ProfileScreen;
diff --git a/src/screens/Amicale/VoteScreen.tsx b/src/screens/Amicale/VoteScreen.tsx
index 8309911..49a0686 100644
--- a/src/screens/Amicale/VoteScreen.tsx
+++ b/src/screens/Amicale/VoteScreen.tsx
@@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see .
*/
-import * as React from 'react';
+import React, { useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import { Button } from 'react-native-paper';
@@ -30,10 +30,10 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
import GENERAL_STYLES from '../../constants/Styles';
-import ConnectionManager from '../../managers/ConnectionManager';
import WebSectionList, {
SectionListDataType,
} from '../../components/Screens/WebSectionList';
+import { useAuthenticatedRequest } from '../../context/loginContext';
export type VoteTeamType = {
id: number;
@@ -65,6 +65,13 @@ type ResponseType = {
dates?: VoteDatesStringType;
};
+type FlatlistType = {
+ teams: Array;
+ hasVoted: boolean;
+ datesString?: VoteDatesStringType;
+ dates?: VoteDatesObjectType;
+};
+
// const FAKE_DATE = {
// "date_begin": "2020-08-19 15:50",
// "date_end": "2020-08-19 15:50",
@@ -113,13 +120,6 @@ type ResponseType = {
// ],
// };
-type PropsType = {};
-
-type StateType = {
- hasVoted: boolean;
- mascotDialogVisible: boolean | undefined;
-};
-
const styles = StyleSheet.create({
button: {
marginLeft: 'auto',
@@ -131,38 +131,19 @@ const styles = StyleSheet.create({
/**
* Screen displaying vote information and controls
*/
-export default class VoteScreen extends React.Component {
- teams: Array;
+export default function VoteScreen() {
+ const [hasVoted, setHasVoted] = useState(false);
+ const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
- hasVoted: boolean;
-
- datesString: undefined | VoteDatesStringType;
-
- dates: undefined | VoteDatesObjectType;
-
- today: Date;
-
- mainFlatListData: SectionListDataType<{ key: string }>;
-
- refreshData: () => void;
-
- constructor(props: PropsType) {
- super(props);
- this.teams = [];
- this.datesString = undefined;
- this.dates = undefined;
- this.state = {
- hasVoted: false,
- mascotDialogVisible: undefined,
- };
- this.hasVoted = false;
- this.today = new Date();
- this.refreshData = () => undefined;
- this.mainFlatListData = [
- { title: '', data: [{ key: 'main' }, { key: 'info' }] },
- ];
- }
+ const datesRequest = useAuthenticatedRequest(
+ 'elections/dates'
+ );
+ const teamsRequest = useAuthenticatedRequest(
+ 'elections/teams'
+ );
+ const today = new Date();
+ const refresh = useRef<() => void | undefined>();
/**
* Gets the string representation of the given date.
*
@@ -173,22 +154,26 @@ export default class VoteScreen extends React.Component {
* @param dateString The string representation of the wanted date
* @returns {string}
*/
- getDateString(date: Date, dateString: string): string {
- if (this.today.getDate() === date.getDate()) {
+ const getDateString = (date: Date, dateString: string) => {
+ if (today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString);
return str != null ? str : '';
}
return dateString;
- }
+ };
- getMainRenderItem = ({ item }: { item: { key: string } }) => {
+ const getMainRenderItem = ({
+ item,
+ }: {
+ item: { key: string; data?: FlatlistType };
+ }) => {
if (item.key === 'info') {
return (
);
}
- return this.getContent();
+ if (item.data) {
+ return getContent(item.data);
+ } else {
+ return ;
+ }
};
- createDataset = (
+ const createDataset = (
data: ResponseType | undefined,
_loading: boolean,
_lastRefreshDate: Date | undefined,
@@ -207,157 +196,158 @@ export default class VoteScreen extends React.Component {
) => {
// data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE;
- this.refreshData = refreshData;
+
+ const mainFlatListData: SectionListDataType<{
+ key: string;
+ data?: FlatlistType;
+ }> = [
+ {
+ title: '',
+ data: [{ key: 'main' }, { key: 'info' }],
+ },
+ ];
+ refresh.current = refreshData;
if (data) {
const { teams, dates } = data;
-
- if (dates && dates.date_begin == null) {
- this.datesString = undefined;
- } else {
- this.datesString = dates;
+ const flatlistData: FlatlistType = {
+ teams: [],
+ hasVoted: false,
+ };
+ if (dates && dates.date_begin != null) {
+ flatlistData.datesString = dates;
}
-
if (teams) {
- this.teams = teams.teams;
- this.hasVoted = teams.has_voted;
+ flatlistData.teams = teams.teams;
+ flatlistData.hasVoted = teams.has_voted;
}
-
- this.generateDateObject();
+ flatlistData.dates = generateDateObject(flatlistData.datesString);
}
- return this.mainFlatListData;
+ return mainFlatListData;
};
- getContent() {
- const { state } = this;
- if (!this.isVoteStarted()) {
- return this.getTeaseVoteCard();
+ const getContent = (data: FlatlistType) => {
+ const { dates } = data;
+ if (!isVoteStarted(dates)) {
+ return getTeaseVoteCard(data);
}
- if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
- return this.getVoteCard();
+ if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
+ return getVoteCard(data);
}
- if (!this.isResultStarted()) {
- return this.getWaitVoteCard();
+ if (!isResultStarted(dates)) {
+ return getWaitVoteCard(data);
}
- if (this.isResultRunning()) {
- return this.getVoteResultCard();
+ if (isResultRunning(dates)) {
+ return getVoteResultCard(data);
}
return ;
- }
-
- onVoteSuccess = (): void => this.setState({ hasVoted: true });
+ };
+ const onVoteSuccess = () => setHasVoted(true);
/**
* The user has not voted yet, and the votes are open
*/
- getVoteCard() {
+ const getVoteCard = (data: FlatlistType) => {
return (
{
+ if (refresh.current) {
+ refresh.current();
+ }
+ }}
/>
);
- }
-
+ };
/**
* Votes have ended, results can be displayed
*/
- getVoteResultCard() {
- if (this.dates != null && this.datesString != null) {
+ const getVoteResultCard = (data: FlatlistType) => {
+ if (data.dates != null && data.datesString != null) {
return (
);
}
return ;
- }
-
+ };
/**
* Vote will open shortly
*/
- getTeaseVoteCard() {
- if (this.dates != null && this.datesString != null) {
+ const getTeaseVoteCard = (data: FlatlistType) => {
+ if (data.dates != null && data.datesString != null) {
return (
);
}
return ;
- }
-
+ };
/**
* Votes have ended, or user has voted waiting for results
*/
- getWaitVoteCard() {
- const { state } = this;
+ const getWaitVoteCard = (data: FlatlistType) => {
let startDate = null;
if (
- this.dates != null &&
- this.datesString != null &&
- this.dates.date_result_begin != null
+ data.dates != null &&
+ data.datesString != null &&
+ data.dates.date_result_begin != null
) {
- startDate = this.getDateString(
- this.dates.date_result_begin,
- this.datesString.date_result_begin
+ startDate = getDateString(
+ data.dates.date_result_begin,
+ data.datesString.date_result_begin
);
}
return (
);
- }
-
- showMascotDialog = () => {
- this.setState({ mascotDialogVisible: true });
};
- hideMascotDialog = () => {
- this.setState({ mascotDialogVisible: false });
+ const showMascotDialog = () => setMascotDialogVisible(true);
+
+ const hideMascotDialog = () => setMascotDialogVisible(false);
+
+ const isVoteStarted = (dates?: VoteDatesObjectType) => {
+ return dates != null && today > dates.date_begin;
};
- isVoteStarted(): boolean {
- return this.dates != null && this.today > this.dates.date_begin;
- }
-
- isResultRunning(): boolean {
+ const isResultRunning = (dates?: VoteDatesObjectType) => {
return (
- this.dates != null &&
- this.today > this.dates.date_result_begin &&
- this.today < this.dates.date_result_end
+ dates != null &&
+ today > dates.date_result_begin &&
+ today < dates.date_result_end
);
- }
+ };
- isResultStarted(): boolean {
- return this.dates != null && this.today > this.dates.date_result_begin;
- }
+ const isResultStarted = (dates?: VoteDatesObjectType) => {
+ return dates != null && today > dates.date_result_begin;
+ };
- isVoteRunning(): boolean {
- return (
- this.dates != null &&
- this.today > this.dates.date_begin &&
- this.today < this.dates.date_end
- );
- }
+ const isVoteRunning = (dates?: VoteDatesObjectType) => {
+ return dates != null && today > dates.date_begin && today < dates.date_end;
+ };
/**
* Generates the objects containing string and Date representations of key vote dates
*/
- generateDateObject() {
- const strings = this.datesString;
- if (strings != null) {
+ const generateDateObject = (
+ strings?: VoteDatesStringType
+ ): VoteDatesObjectType | undefined => {
+ if (strings) {
const dateBegin = stringToDate(strings.date_begin);
const dateEnd = stringToDate(strings.date_end);
const dateResultBegin = stringToDate(strings.date_result_begin);
@@ -368,27 +358,25 @@ export default class VoteScreen extends React.Component {
dateResultBegin != null &&
dateResultEnd != null
) {
- this.dates = {
+ return {
date_begin: dateBegin,
date_end: dateEnd,
date_result_begin: dateResultBegin,
date_result_end: dateResultEnd,
};
} else {
- this.dates = undefined;
+ return undefined;
}
} else {
- this.dates = undefined;
+ return undefined;
}
- }
+ };
- request = () => {
+ const request = () => {
return new Promise((resolve: (data: ResponseType) => void) => {
- ConnectionManager.getInstance()
- .authenticatedRequest('elections/dates')
+ datesRequest()
.then((datesData) => {
- ConnectionManager.getInstance()
- .authenticatedRequest('elections/teams')
+ teamsRequest()
.then((teamsData) => {
resolve({
dates: datesData,
@@ -405,38 +393,28 @@ export default class VoteScreen extends React.Component {
});
};
- /**
- * Renders the authenticated screen.
- *
- * Teams and dates are not mandatory to allow showing the information box even if api requests fail
- *
- * @returns {*}
- */
- render() {
- const { state } = this;
- return (
-
-
-
-
- );
- }
+ return (
+
+
+
+
+ );
}
diff --git a/src/screens/Home/HomeScreen.tsx b/src/screens/Home/HomeScreen.tsx
index e14cb9d..c129034 100644
--- a/src/screens/Home/HomeScreen.tsx
+++ b/src/screens/Home/HomeScreen.tsx
@@ -46,7 +46,6 @@ import MaterialHeaderButtons, {
Item,
} from '../../components/Overrides/CustomHeaderButton';
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
-import ConnectionManager from '../../managers/ConnectionManager';
import LogoutDialog from '../../components/Amicale/LogoutDialog';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
@@ -59,6 +58,7 @@ import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
import { ServiceItemType } from '../../utils/Services';
import { useCurrentDashboard } from '../../context/preferencesContext';
import { MainRoutes } from '../../navigation/MainNavigator';
+import { useLoginState } from '../../context/loginContext';
const FEED_ITEM_HEIGHT = 500;
@@ -146,9 +146,7 @@ function HomeScreen(props: Props) {
const [dialogVisible, setDialogVisible] = useState(false);
const fabRef = useRef(null);
- const [isLoggedIn, setIsLoggedIn] = useState(
- ConnectionManager.getInstance().isLoggedIn()
- );
+ const isLoggedIn = useLoginState();
const { currentDashboard } = useCurrentDashboard();
let homeDashboard: FullDashboardType | null = null;
@@ -199,13 +197,8 @@ function HomeScreen(props: Props) {
}
}
};
-
- if (ConnectionManager.getInstance().isLoggedIn() !== isLoggedIn) {
- setIsLoggedIn(ConnectionManager.getInstance().isLoggedIn());
- }
// handle link open when home is not focused or created
handleNavigationParams();
- return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoggedIn])
);
diff --git a/src/utils/WebData.ts b/src/utils/WebData.ts
index 854c69a..ba4117e 100644
--- a/src/utils/WebData.ts
+++ b/src/utils/WebData.ts
@@ -80,7 +80,8 @@ export function isApiResponseValid(response: ApiResponseType): boolean {
export async function apiRequest(
path: string,
method: string,
- params?: object
+ params?: object,
+ token?: string
): Promise {
return new Promise(
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
@@ -88,7 +89,9 @@ export async function apiRequest(
if (params != null) {
requestParams = { ...params };
}
- console.log(Urls.amicale.api + path);
+ if (token) {
+ requestParams = { ...requestParams, token: token };
+ }
fetch(Urls.amicale.api + path, {
method,
@@ -135,6 +138,33 @@ export async function apiRequest(
);
}
+export async function connectToAmicale(email: string, password: string) {
+ return new Promise(
+ (
+ resolve: (token: string) => void,
+ reject: (error: ApiRejectType) => void
+ ) => {
+ const data = {
+ email,
+ password,
+ };
+ apiRequest('password', 'POST', data)
+ .then((response: ApiDataLoginType) => {
+ if (response.token != null) {
+ resolve(response.token);
+ } else {
+ reject({
+ status: REQUEST_STATUS.SERVER_ERROR,
+ });
+ }
+ })
+ .catch((err) => {
+ reject(err);
+ });
+ }
+ );
+}
+
/**
* Reads data from the given url and returns it.
*
diff --git a/src/utils/loginToken.ts b/src/utils/loginToken.ts
new file mode 100644
index 0000000..f1c6b1b
--- /dev/null
+++ b/src/utils/loginToken.ts
@@ -0,0 +1,46 @@
+import * as Keychain from 'react-native-keychain';
+
+/**
+ * Tries to recover login token from the secure keychain
+ *
+ * @returns Promise
+ */
+export async function retrieveLoginToken(): Promise {
+ return new Promise((resolve: (token: string | undefined) => void) => {
+ Keychain.getGenericPassword()
+ .then((data: Keychain.UserCredentials | false) => {
+ if (data && data.password) {
+ resolve(data.password);
+ } else {
+ resolve(undefined);
+ }
+ })
+ .catch(() => resolve(undefined));
+ });
+}
+/**
+ * Saves the login token in the secure keychain
+ *
+ * @param email
+ * @param token
+ * @returns Promise
+ */
+export async function saveLoginToken(
+ email: string,
+ token: string
+): Promise {
+ return new Promise((resolve: () => void, reject: () => void) => {
+ Keychain.setGenericPassword(email, token).then(resolve).catch(reject);
+ });
+}
+
+/**
+ * Deletes the login token from the keychain
+ *
+ * @returns Promise
+ */
+export async function deleteLoginToken(): Promise {
+ return new Promise((resolve: () => void, reject: () => void) => {
+ Keychain.resetGenericPassword().then(resolve).catch(reject);
+ });
+}
diff --git a/src/utils/logout.ts b/src/utils/logout.ts
new file mode 100644
index 0000000..6c13b03
--- /dev/null
+++ b/src/utils/logout.ts
@@ -0,0 +1,11 @@
+import { useCallback } from 'react';
+import { useLogin } from '../context/loginContext';
+
+export const useLogout = () => {
+ const { setLogin } = useLogin();
+
+ const onLogout = useCallback(() => {
+ setLogin(undefined);
+ }, [setLogin]);
+ return onLogout;
+};