forked from vergnet/application-amicale
		
	convert connection manager to context
This commit is contained in:
		
							parent
							
								
									44aa52b3aa
								
							
						
					
					
						commit
						541c002558
					
				
					 24 changed files with 1610 additions and 1965 deletions
				
			
		
							
								
								
									
										15
									
								
								App.tsx
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								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> { | |||
|             <MascotPreferencesProvider | ||||
|               initialPreferences={this.state.initialPreferences.mascot} | ||||
|             > | ||||
|               <LoginProvider initialToken={this.state.loginToken}> | ||||
|                 <MainApp | ||||
|                   ref={this.navigatorRef} | ||||
|                   defaultHomeData={this.defaultHomeData} | ||||
|                   defaultHomeRoute={this.defaultHomeRoute} | ||||
|                 /> | ||||
|               </LoginProvider> | ||||
|             </MascotPreferencesProvider> | ||||
|           </ProxiwashPreferencesProvider> | ||||
|         </PlanexPreferencesProvider> | ||||
|  |  | |||
							
								
								
									
										231
									
								
								src/components/Amicale/Login/LoginForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/components/Amicale/Login/LoginForm.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<RNTextInput>(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 ( | ||||
|     <View style={styles.card}> | ||||
|       <Card.Title | ||||
|         title={i18n.t('screens.login.title')} | ||||
|         titleStyle={styles.text} | ||||
|         subtitle={i18n.t('screens.login.subtitle')} | ||||
|         subtitleStyle={styles.text} | ||||
|         left={({ size }) => ( | ||||
|           <Image | ||||
|             source={ICON_AMICALE} | ||||
|             style={{ | ||||
|               width: size, | ||||
|               height: size, | ||||
|             }} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|       <Card.Content> | ||||
|         <View> | ||||
|           <TextInput | ||||
|             label={i18n.t('screens.login.email')} | ||||
|             mode={'outlined'} | ||||
|             value={email} | ||||
|             onChangeText={onEmailChange} | ||||
|             onBlur={validateEmail} | ||||
|             onSubmitEditing={onEmailSubmit} | ||||
|             error={shouldShowEmailError()} | ||||
|             textContentType={'emailAddress'} | ||||
|             autoCapitalize={'none'} | ||||
|             autoCompleteType={'email'} | ||||
|             autoCorrect={false} | ||||
|             keyboardType={'email-address'} | ||||
|             returnKeyType={'next'} | ||||
|             secureTextEntry={false} | ||||
|           /> | ||||
|           <HelperText type={'error'} visible={shouldShowEmailError()}> | ||||
|             {i18n.t('screens.login.emailError')} | ||||
|           </HelperText> | ||||
|           <TextInput | ||||
|             ref={passwordRef} | ||||
|             label={i18n.t('screens.login.password')} | ||||
|             mode={'outlined'} | ||||
|             value={password} | ||||
|             onChangeText={onPasswordChange} | ||||
|             onBlur={validatePassword} | ||||
|             onSubmitEditing={onSubmit} | ||||
|             error={shouldShowPasswordError()} | ||||
|             textContentType={'password'} | ||||
|             autoCapitalize={'none'} | ||||
|             autoCompleteType={'password'} | ||||
|             autoCorrect={false} | ||||
|             keyboardType={'default'} | ||||
|             returnKeyType={'done'} | ||||
|             secureTextEntry={true} | ||||
|           /> | ||||
|           <HelperText type={'error'} visible={shouldShowPasswordError()}> | ||||
|             {i18n.t('screens.login.passwordError')} | ||||
|           </HelperText> | ||||
|         </View> | ||||
|         <Card.Actions style={styles.buttonContainer}> | ||||
|           <Button | ||||
|             icon="lock-question" | ||||
|             mode="contained" | ||||
|             onPress={props.onResetPasswordPress} | ||||
|             color={theme.colors.warning} | ||||
|             style={styles.lockButton} | ||||
|           > | ||||
|             {i18n.t('screens.login.resetPassword')} | ||||
|           </Button> | ||||
|           <Button | ||||
|             icon="send" | ||||
|             mode="contained" | ||||
|             disabled={!shouldEnableLogin()} | ||||
|             loading={props.loading} | ||||
|             onPress={onSubmit} | ||||
|             style={styles.sendButton} | ||||
|           > | ||||
|             {i18n.t('screens.login.title')} | ||||
|           </Button> | ||||
|         </Card.Actions> | ||||
|         <Card.Actions> | ||||
|           <Button | ||||
|             icon="help-circle" | ||||
|             mode="contained" | ||||
|             onPress={props.onHelpPress} | ||||
|             style={GENERAL_STYLES.centerHorizontal} | ||||
|           > | ||||
|             {i18n.t('screens.login.mascotDialog.title')} | ||||
|           </Button> | ||||
|         </Card.Actions> | ||||
|       </Card.Content> | ||||
|     </View> | ||||
|   ); | ||||
| } | ||||
|  | @ -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,20 +28,13 @@ 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<void> => { | ||||
|     return new Promise((resolve: () => void) => { | ||||
|       ConnectionManager.getInstance() | ||||
|         .disconnect() | ||||
|         .then(() => { | ||||
|           navigation.reset({ | ||||
|             index: 0, | ||||
|             routes: [{ name: 'main' }], | ||||
|           }); | ||||
|           props.onDismiss(); | ||||
|       onLogout(); | ||||
|       resolve(); | ||||
|     }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|  |  | |||
							
								
								
									
										100
									
								
								src/components/Amicale/Profile/ProfileClubCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/components/Amicale/Profile/ProfileClubCard.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<ProfileClubType>; | ||||
| }; | ||||
| 
 | ||||
| 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; | ||||
|       }; | ||||
|     }) => ( | ||||
|       <List.Icon | ||||
|         color={leftProps.color} | ||||
|         style={leftProps.style} | ||||
|         icon="chevron-right" | ||||
|       /> | ||||
|     ); | ||||
|     if (item.is_manager) { | ||||
|       description = i18n.t('screens.profile.isManager'); | ||||
|       icon = (leftProps) => ( | ||||
|         <List.Icon | ||||
|           style={leftProps.style} | ||||
|           icon="star" | ||||
|           color={theme.colors.primary} | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
|     return ( | ||||
|       <List.Item | ||||
|         title={item.name} | ||||
|         description={description} | ||||
|         left={icon} | ||||
|         onPress={onPress} | ||||
|       /> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   function getClubList(list: Array<ProfileClubType> | undefined) { | ||||
|     if (!list) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     list.sort((a) => (a.is_manager ? -1 : 1)); | ||||
|     return ( | ||||
|       <FlatList | ||||
|         renderItem={getClubListItem} | ||||
|         keyExtractor={clubKeyExtractor} | ||||
|         data={list} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Card style={styles.card}> | ||||
|       <Card.Title | ||||
|         title={i18n.t('screens.profile.clubs')} | ||||
|         subtitle={i18n.t('screens.profile.clubsSubtitle')} | ||||
|         left={(iconProps) => ( | ||||
|           <Avatar.Icon | ||||
|             size={iconProps.size} | ||||
|             icon="account-group" | ||||
|             color={theme.colors.primary} | ||||
|             style={styles.icon} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|       <Card.Content> | ||||
|         <Divider /> | ||||
|         {getClubList(props.clubs)} | ||||
|       </Card.Content> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										56
									
								
								src/components/Amicale/Profile/ProfileMembershipCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/components/Amicale/Profile/ProfileMembershipCard.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 ( | ||||
|     <Card style={styles.card}> | ||||
|       <Card.Title | ||||
|         title={i18n.t('screens.profile.membership')} | ||||
|         subtitle={i18n.t('screens.profile.membershipSubtitle')} | ||||
|         left={(iconProps) => ( | ||||
|           <Avatar.Icon | ||||
|             size={iconProps.size} | ||||
|             icon="credit-card" | ||||
|             color={theme.colors.primary} | ||||
|             style={styles.icon} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|       <Card.Content> | ||||
|         <List.Section> | ||||
|           <List.Item | ||||
|             title={ | ||||
|               state | ||||
|                 ? i18n.t('screens.profile.membershipPayed') | ||||
|                 : i18n.t('screens.profile.membershipNotPayed') | ||||
|             } | ||||
|             left={(leftProps) => ( | ||||
|               <List.Icon | ||||
|                 style={leftProps.style} | ||||
|                 color={state ? theme.colors.success : theme.colors.danger} | ||||
|                 icon={state ? 'check' : 'close'} | ||||
|               /> | ||||
|             )} | ||||
|           /> | ||||
|         </List.Section> | ||||
|       </Card.Content> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										110
									
								
								src/components/Amicale/Profile/ProfilePersonalCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/components/Amicale/Profile/ProfilePersonalCard.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 ( | ||||
|       <List.Item | ||||
|         title={title} | ||||
|         description={subtitle} | ||||
|         left={(leftProps) => ( | ||||
|           <List.Icon | ||||
|             style={leftProps.style} | ||||
|             icon={icon} | ||||
|             color={field != null ? leftProps.color : theme.colors.textDisabled} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Card style={styles.card}> | ||||
|       <Card.Title | ||||
|         title={`${profile?.first_name} ${profile?.last_name}`} | ||||
|         subtitle={profile?.email} | ||||
|         left={(iconProps) => ( | ||||
|           <Avatar.Icon | ||||
|             size={iconProps.size} | ||||
|             icon="account" | ||||
|             color={theme.colors.primary} | ||||
|             style={styles.icon} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|       <Card.Content> | ||||
|         <Divider /> | ||||
|         <List.Section> | ||||
|           <List.Subheader> | ||||
|             {i18n.t('screens.profile.personalInformation')} | ||||
|           </List.Subheader> | ||||
|           {getPersonalListItem(profile?.birthday, 'cake-variant')} | ||||
|           {getPersonalListItem(profile?.phone, 'phone')} | ||||
|           {getPersonalListItem(profile?.email, 'email')} | ||||
|           {getPersonalListItem(profile?.branch, 'school')} | ||||
|         </List.Section> | ||||
|         <Divider /> | ||||
|         <Card.Actions> | ||||
|           <Button | ||||
|             icon="account-edit" | ||||
|             mode="contained" | ||||
|             onPress={() => { | ||||
|               navigation.navigate('website', { | ||||
|                 host: Urls.websites.amicale, | ||||
|                 path: profile?.link, | ||||
|                 title: i18n.t('screens.websites.amicale'), | ||||
|               }); | ||||
|             }} | ||||
|             style={styles.editButton} | ||||
|           > | ||||
|             {i18n.t('screens.profile.editInformation')} | ||||
|           </Button> | ||||
|         </Card.Actions> | ||||
|       </Card.Content> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										81
									
								
								src/components/Amicale/Profile/ProfileWelcomeCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/components/Amicale/Profile/ProfileWelcomeCard.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 ( | ||||
|     <Card style={styles.card}> | ||||
|       <Card.Title | ||||
|         title={i18n.t('screens.profile.welcomeTitle', { | ||||
|           name: props.firstname, | ||||
|         })} | ||||
|         left={() => ( | ||||
|           <Mascot | ||||
|             style={styles.mascot} | ||||
|             emotion={MASCOT_STYLE.COOL} | ||||
|             animated | ||||
|             entryAnimation={{ | ||||
|               animation: 'bounceIn', | ||||
|               duration: 1000, | ||||
|             }} | ||||
|           /> | ||||
|         )} | ||||
|         titleStyle={styles.title} | ||||
|       /> | ||||
|       <Card.Content> | ||||
|         <Divider /> | ||||
|         <Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph> | ||||
|         <CardList | ||||
|           dataset={getAmicaleServices(navigation.navigate, [ | ||||
|             SERVICES_KEY.PROFILE, | ||||
|           ])} | ||||
|           isHorizontal={true} | ||||
|         /> | ||||
|         <Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph> | ||||
|         <Divider /> | ||||
|         <Card.Actions> | ||||
|           <Button | ||||
|             icon="bug" | ||||
|             mode="contained" | ||||
|             onPress={() => { | ||||
|               navigation.navigate('feedback'); | ||||
|             }} | ||||
|             style={styles.editButton} | ||||
|           > | ||||
|             {i18n.t('screens.feedback.homeButtonTitle')} | ||||
|           </Button> | ||||
|         </Card.Actions> | ||||
|       </Card.Content> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export default React.memo( | ||||
|   ProfileWelcomeCard, | ||||
|   (pp, np) => pp.firstname === np.firstname | ||||
| ); | ||||
|  | @ -17,30 +17,23 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<VoteTeamType>; | ||||
|   onVoteSuccess: () => void; | ||||
|   onVoteError: () => void; | ||||
| }; | ||||
| 
 | ||||
| type StateType = { | ||||
|   selectedTeam: string; | ||||
|   voteDialogVisible: boolean; | ||||
|   errorDialogVisible: boolean; | ||||
|   currentError: ApiRejectType; | ||||
| }; | ||||
| 
 | ||||
| const styles = StyleSheet.create({ | ||||
|   card: { | ||||
|     margin: 10, | ||||
|  | @ -50,68 +43,47 @@ 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<ApiRejectType>({ | ||||
|     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 }) => ( | ||||
|     <RadioButton.Item label={item.name} value={item.id.toString()} /> | ||||
|   ); | ||||
| 
 | ||||
|   showVoteDialog = (): void => this.setState({ voteDialogVisible: true }); | ||||
|   const showVoteDialog = () => setVoteDialogVisible(true); | ||||
| 
 | ||||
|   onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false }); | ||||
|   const onVoteDialogDismiss = () => setVoteDialogVisible(false); | ||||
| 
 | ||||
|   onVoteDialogAccept = async (): Promise<void> => { | ||||
|   const onVoteDialogAccept = async (): Promise<void> => { | ||||
|     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 ( | ||||
|     <View> | ||||
|       <Card style={styles.card}> | ||||
|  | @ -124,44 +96,45 @@ export default class VoteSelect extends React.PureComponent< | |||
|         /> | ||||
|         <Card.Content> | ||||
|           <RadioButton.Group | ||||
|               onValueChange={this.onVoteSelectionChange} | ||||
|               value={state.selectedTeam} | ||||
|             onValueChange={setSelectedTeam} | ||||
|             value={selectedTeam} | ||||
|           > | ||||
|             <FlatList | ||||
|               data={props.teams} | ||||
|                 keyExtractor={this.voteKeyExtractor} | ||||
|                 extraData={state.selectedTeam} | ||||
|                 renderItem={this.voteRenderItem} | ||||
|               keyExtractor={voteKeyExtractor} | ||||
|               extraData={selectedTeam} | ||||
|               renderItem={voteRenderItem} | ||||
|             /> | ||||
|           </RadioButton.Group> | ||||
|         </Card.Content> | ||||
|         <Card.Actions> | ||||
|           <Button | ||||
|               icon="send" | ||||
|               mode="contained" | ||||
|               onPress={this.showVoteDialog} | ||||
|             icon={'send'} | ||||
|             mode={'contained'} | ||||
|             onPress={showVoteDialog} | ||||
|             style={styles.button} | ||||
|               disabled={state.selectedTeam === 'none'} | ||||
|             disabled={selectedTeam === 'none'} | ||||
|           > | ||||
|             {i18n.t('screens.vote.select.sendButton')} | ||||
|           </Button> | ||||
|         </Card.Actions> | ||||
|       </Card> | ||||
|       <LoadingConfirmDialog | ||||
|           visible={state.voteDialogVisible} | ||||
|           onDismiss={this.onVoteDialogDismiss} | ||||
|           onAccept={this.onVoteDialogAccept} | ||||
|         visible={voteDialogVisible} | ||||
|         onDismiss={onVoteDialogDismiss} | ||||
|         onAccept={onVoteDialogAccept} | ||||
|         title={i18n.t('screens.vote.select.dialogTitle')} | ||||
|         titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')} | ||||
|         message={i18n.t('screens.vote.select.dialogMessage')} | ||||
|       /> | ||||
|       <ErrorDialog | ||||
|           visible={state.errorDialogVisible} | ||||
|           onDismiss={this.onErrorDialogDismiss} | ||||
|           status={state.currentError.status} | ||||
|           code={state.currentError.code} | ||||
|         visible={currentError.status !== REQUEST_STATUS.SUCCESS} | ||||
|         onDismiss={onErrorDialogDismiss} | ||||
|         status={currentError.status} | ||||
|         code={currentError.code} | ||||
|       /> | ||||
|     </View> | ||||
|   ); | ||||
| } | ||||
| } | ||||
| 
 | ||||
| export default VoteSelect; | ||||
|  |  | |||
|  | @ -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<any>; | ||||
|   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); | ||||
|  |  | |||
|  | @ -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<T> = { | ||||
|   request: () => Promise<T>; | ||||
|  | @ -44,6 +44,7 @@ type Props<T> = RequestScreenProps<T>; | |||
| const MIN_REFRESH_TIME = 3 * 1000; | ||||
| 
 | ||||
| export default function RequestScreen<T>(props: Props<T>) { | ||||
|   const onLogout = useLogout(); | ||||
|   const navigation = useNavigation<StackNavigationProp<any>>(); | ||||
|   const route = useRoute(); | ||||
|   const refreshInterval = useRef<number>(); | ||||
|  | @ -103,13 +104,10 @@ export default function RequestScreen<T>(props: Props<T>) { | |||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (isErrorCritical(code)) { | ||||
|       ConnectionManager.getInstance() | ||||
|         .disconnect() | ||||
|         .then(() => { | ||||
|       onLogout(); | ||||
|       navigation.replace(MainRoutes.Login, { nextScreen: route.name }); | ||||
|         }); | ||||
|     } | ||||
|   }, [code, navigation, route]); | ||||
|   }, [code, navigation, route, onLogout]); | ||||
| 
 | ||||
|   if (data === undefined && loading && props.showLoading !== false) { | ||||
|     return <BasicLoadingScreen />; | ||||
|  |  | |||
							
								
								
									
										27
									
								
								src/components/providers/LoginProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/components/providers/LoginProvider.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<LoginContextType>({ | ||||
|     token: props.initialToken, | ||||
|     setLogin: setLogin, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <LoginContext.Provider value={loginState}> | ||||
|       {props.children} | ||||
|     </LoginContext.Provider> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/context/loginContext.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/context/loginContext.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<LoginContextType>({ | ||||
|   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<T>( | ||||
|   path: string, | ||||
|   params?: { [key: string]: any } | ||||
| ) { | ||||
|   const token = useLoginToken(); | ||||
|   return () => apiRequest<T>(path, 'POST', params, token); | ||||
| } | ||||
|  | @ -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 <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<void> | ||||
|    */ | ||||
|   async recoverLogin(): Promise<void> { | ||||
|     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<void> | ||||
|    */ | ||||
|   async saveLogin(_email: string, token: string): Promise<void> { | ||||
|     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<void> | ||||
|    */ | ||||
|   async disconnect(): Promise<void> { | ||||
|     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<void> | ||||
|    */ | ||||
|   async connect(email: string, password: string): Promise<void> { | ||||
|     return new Promise( | ||||
|       (resolve: () => void, reject: (error: ApiRejectType) => void) => { | ||||
|         const data = { | ||||
|           email, | ||||
|           password, | ||||
|         }; | ||||
|         apiRequest<ApiDataLoginType>(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<ApiGenericDataType> | ||||
|    */ | ||||
|   async authenticatedRequest<T>( | ||||
|     path: string, | ||||
|     params?: { [key: string]: any } | ||||
|   ): Promise<T> { | ||||
|     return new Promise( | ||||
|       ( | ||||
|         resolve: (response: T) => void, | ||||
|         reject: (error: ApiRejectType) => void | ||||
|       ) => { | ||||
|         if (this.getToken() !== null) { | ||||
|           const data = { | ||||
|             ...params, | ||||
|             token: this.getToken(), | ||||
|           }; | ||||
|           apiRequest<T>(path, 'POST', data) | ||||
|             .then((response: T) => resolve(response)) | ||||
|             .catch(reject); | ||||
|         } else { | ||||
|           reject({ | ||||
|             status: REQUEST_STATUS.TOKEN_RETRIEVE, | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -17,7 +17,7 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<any>; | ||||
| 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<PropsType> { | ||||
|   displayData: ClubType | undefined; | ||||
| function ClubDisplayScreen(props: Props) { | ||||
|   const navigation = useNavigation(); | ||||
|   const theme = useTheme(); | ||||
| 
 | ||||
|   categories: Array<ClubCategoryType> | null; | ||||
|   const [displayData, setDisplayData] = useState<ClubType | undefined>(); | ||||
|   const [categories, setCategories] = useState< | ||||
|     Array<ClubCategoryType> | undefined | ||||
|   >(); | ||||
|   const [clubId, setClubId] = useState<number | undefined>(); | ||||
| 
 | ||||
|   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<PropsType> { | |||
|    * @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<PropsType> { | |||
|    * @param categories The categories to display (max 2) | ||||
|    * @returns {null|*} | ||||
|    */ | ||||
|   getCategoriesRender(categories: Array<number | null>) { | ||||
|     if (this.categories == null) { | ||||
|   const getCategoriesRender = (c: Array<number | null>) => { | ||||
|     if (!categories) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     const final: Array<React.ReactNode> = []; | ||||
|     categories.forEach((cat: number | null) => { | ||||
|     c.forEach((cat: number | null) => { | ||||
|       if (cat != null) { | ||||
|         final.push( | ||||
|           <Chip style={styles.category} key={cat}> | ||||
|             {this.getCategoryName(cat)} | ||||
|             {getCategoryName(cat)} | ||||
|           </Chip> | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|     return <View style={styles.categoryContainer}>{final}</View>; | ||||
|   } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Gets the view for rendering club managers if any | ||||
|  | @ -166,8 +161,7 @@ class ClubDisplayScreen extends React.Component<PropsType> { | |||
|    * @param email The club contact email | ||||
|    * @returns {*} | ||||
|    */ | ||||
|   getManagersRender(managers: Array<string>, email: string | null) { | ||||
|     const { props } = this; | ||||
|   const getManagersRender = (managers: Array<string>, email: string | null) => { | ||||
|     const managersListView: Array<React.ReactNode> = []; | ||||
|     managers.forEach((item: string) => { | ||||
|       managersListView.push(<Paragraph key={item}>{item}</Paragraph>); | ||||
|  | @ -191,22 +185,18 @@ class ClubDisplayScreen extends React.Component<PropsType> { | |||
|             <Avatar.Icon | ||||
|               size={iconProps.size} | ||||
|               style={styles.icon} | ||||
|               color={ | ||||
|                 hasManagers | ||||
|                   ? props.theme.colors.success | ||||
|                   : props.theme.colors.primary | ||||
|               } | ||||
|               color={hasManagers ? theme.colors.success : theme.colors.primary} | ||||
|               icon="account-tie" | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|         <Card.Content> | ||||
|           {managersListView} | ||||
|           {ClubDisplayScreen.getEmailButton(email, hasManagers)} | ||||
|           {getEmailButton(email, hasManagers)} | ||||
|         </Card.Content> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * 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<PropsType> { | |||
|    * @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<PropsType> { | |||
|         </Button> | ||||
|       </Card.Actions> | ||||
|     ); | ||||
|   } | ||||
|   }; | ||||
| 
 | ||||
|   getScreen = (data: ResponseType | undefined) => { | ||||
|   const getScreen = (data: ResponseType | undefined) => { | ||||
|     if (data) { | ||||
|       this.updateHeaderTitle(data); | ||||
|       updateHeaderTitle(data); | ||||
|       return ( | ||||
|         <CollapsibleScrollView style={styles.scroll} hasTab> | ||||
|           {this.getCategoriesRender(data.category)} | ||||
|           {getCategoriesRender(data.category)} | ||||
|           {data.logo !== null ? ( | ||||
|             <ImageGalleryButton | ||||
|               images={[{ url: data.logo }]} | ||||
|  | @ -261,7 +251,7 @@ class ClubDisplayScreen extends React.Component<PropsType> { | |||
|           ) : ( | ||||
|             <View /> | ||||
|           )} | ||||
|           {this.getManagersRender(data.responsibles, data.email)} | ||||
|           {getManagersRender(data.responsibles, data.email)} | ||||
|         </CollapsibleScrollView> | ||||
|       ); | ||||
|     } | ||||
|  | @ -273,27 +263,22 @@ class ClubDisplayScreen extends React.Component<PropsType> { | |||
|    * | ||||
|    * @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 }); | ||||
|   }; | ||||
| 
 | ||||
|   const request = useAuthenticatedRequest<ClubType>('clubs/info', { | ||||
|     id: clubId, | ||||
|   }); | ||||
| 
 | ||||
|   render() { | ||||
|     if (this.shouldFetchData) { | ||||
|   return ( | ||||
|     <RequestScreen | ||||
|           request={() => | ||||
|             ConnectionManager.getInstance().authenticatedRequest<ResponseType>( | ||||
|               'clubs/info', | ||||
|               { id: this.clubId } | ||||
|             ) | ||||
|           } | ||||
|           render={this.getScreen} | ||||
|       request={request} | ||||
|       render={getScreen} | ||||
|       cache={displayData} | ||||
|       onCacheUpdate={setDisplayData} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
|     return this.getScreen(this.displayData); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default withTheme(ClubDisplayScreen); | ||||
| export default ClubDisplayScreen; | ||||
|  |  | |||
|  | @ -17,11 +17,10 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<string>; | ||||
| }; | ||||
| 
 | ||||
| type PropsType = { | ||||
|   navigation: StackNavigationProp<any>; | ||||
| }; | ||||
| 
 | ||||
| type StateType = { | ||||
|   currentlySelectedCategories: Array<number>; | ||||
|   currentSearchString: string; | ||||
| }; | ||||
| 
 | ||||
| type ResponseType = { | ||||
|   categories: Array<ClubCategoryType>; | ||||
|   clubs: Array<ClubType>; | ||||
|  | @ -65,33 +56,52 @@ type ResponseType = { | |||
| 
 | ||||
| const LIST_ITEM_HEIGHT = 96; | ||||
| 
 | ||||
| class ClubListScreen extends React.Component<PropsType, StateType> { | ||||
|   categories: Array<ClubCategoryType>; | ||||
| function ClubListScreen() { | ||||
|   const navigation = useNavigation(); | ||||
|   const request = useAuthenticatedRequest<ResponseType>('clubs/list'); | ||||
|   const [ | ||||
|     currentlySelectedCategories, | ||||
|     setCurrentlySelectedCategories, | ||||
|   ] = useState<Array<number>>([]); | ||||
|   const [currentSearchString, setCurrentSearchString] = useState(''); | ||||
|   const categories = useRef<Array<ClubCategoryType>>([]); | ||||
| 
 | ||||
|   constructor(props: PropsType) { | ||||
|     super(props); | ||||
|     this.categories = []; | ||||
|     this.state = { | ||||
|       currentlySelectedCategories: [], | ||||
|       currentSearchString: '', | ||||
|   useLayoutEffect(() => { | ||||
|     const getSearchBar = () => { | ||||
|       return ( | ||||
|         // @ts-ignore
 | ||||
|         <Searchbar | ||||
|           placeholder={i18n.t('screens.proximo.search')} | ||||
|           onChangeText={onSearchStringChange} | ||||
|         /> | ||||
|       ); | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Creates the header content | ||||
|    */ | ||||
|   componentDidMount() { | ||||
|     const { props } = this; | ||||
|     props.navigation.setOptions({ | ||||
|       headerTitle: this.getSearchBar, | ||||
|       headerRight: this.getHeaderButtons, | ||||
|     const getHeaderButtons = () => { | ||||
|       return ( | ||||
|         <MaterialHeaderButtons> | ||||
|           <Item | ||||
|             title="main" | ||||
|             iconName="information" | ||||
|             onPress={() => navigation.navigate('club-about')} | ||||
|           /> | ||||
|         </MaterialHeaderButtons> | ||||
|       ); | ||||
|     }; | ||||
|     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<PropsType, StateType> { | |||
|    * | ||||
|    * @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
 | ||||
|       <Searchbar | ||||
|         placeholder={i18n.t('screens.proximo.search')} | ||||
|         onChangeText={this.onSearchStringChange} | ||||
|       /> | ||||
|     ); | ||||
|   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 ( | ||||
|       <MaterialHeaderButtons> | ||||
|         <Item title="main" iconName="information" onPress={onPress} /> | ||||
|       </MaterialHeaderButtons> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   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<PropsType, StateType> { | |||
|    * | ||||
|    * @returns {*} | ||||
|    */ | ||||
|   getListHeader(data: ResponseType | undefined) { | ||||
|     const { state } = this; | ||||
|   const getListHeader = (data: ResponseType | undefined) => { | ||||
|     if (data) { | ||||
|       return ( | ||||
|         <ClubListHeader | ||||
|           categories={this.categories} | ||||
|           selectedCategories={state.currentlySelectedCategories} | ||||
|           onChipSelect={this.onChipSelect} | ||||
|           categories={categories.current} | ||||
|           selectedCategories={currentlySelectedCategories} | ||||
|           onChipSelect={onChipSelect} | ||||
|         /> | ||||
|       ); | ||||
|     } 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<PropsType, StateType> { | |||
|     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 ( | ||||
|         <ClubListItem | ||||
|           categoryTranslator={this.getCategoryOfId} | ||||
|           categoryTranslator={getCategoryOfId} | ||||
|           item={item} | ||||
|           onPress={onPress} | ||||
|           height={LIST_ITEM_HEIGHT} | ||||
|  | @ -213,7 +175,7 @@ class ClubListScreen extends React.Component<PropsType, StateType> { | |||
|     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<PropsType, StateType> { | |||
|    * @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<PropsType, StateType> { | |||
|       } | ||||
|     } | ||||
|     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<PropsType, StateType> { | |||
|    * @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 ( | ||||
|     <WebSectionList | ||||
|         request={() => | ||||
|           ConnectionManager.getInstance().authenticatedRequest<ResponseType>( | ||||
|             'clubs/list' | ||||
|           ) | ||||
|         } | ||||
|         createDataset={this.createDataset} | ||||
|         keyExtractor={this.keyExtractor} | ||||
|         renderItem={this.getRenderItem} | ||||
|         renderListHeaderComponent={(data) => this.getListHeader(data)} | ||||
|       request={request} | ||||
|       createDataset={createDataset} | ||||
|       keyExtractor={keyExtractor} | ||||
|       renderItem={getRenderItem} | ||||
|       renderListHeaderComponent={getListHeader} | ||||
|       // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
 | ||||
|       removeClippedSubviews={true} | ||||
|       itemHeight={LIST_ITEM_HEIGHT} | ||||
|     /> | ||||
|   ); | ||||
| } | ||||
| } | ||||
| 
 | ||||
| export default ClubListScreen; | ||||
|  |  | |||
|  | @ -17,26 +17,17 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<any>; | ||||
| }; | ||||
| 
 | ||||
| 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<PropsType, StateType> { | ||||
|   userRents: null | Array<RentedDeviceType>; | ||||
| function EquipmentListScreen() { | ||||
|   const userRents = useRef<undefined | Array<RentedDeviceType>>(); | ||||
|   const [mascotDialogVisible, setMascotDialogVisible] = useState(false); | ||||
| 
 | ||||
|   constructor(props: PropsType) { | ||||
|     super(props); | ||||
|     this.userRents = null; | ||||
|     this.state = { | ||||
|       mascotDialogVisible: undefined, | ||||
|     }; | ||||
|   } | ||||
|   const requestAll = useAuthenticatedRequest<{ devices: Array<DeviceType> }>( | ||||
|     'location/all' | ||||
|   ); | ||||
|   const requestOwn = useAuthenticatedRequest<{ | ||||
|     locations: Array<RentedDeviceType>; | ||||
|   }>('location/my'); | ||||
| 
 | ||||
|   getRenderItem = ({ item }: { item: DeviceType }) => { | ||||
|     const { navigation } = this.props; | ||||
|   const getRenderItem = ({ item }: { item: DeviceType }) => { | ||||
|     return ( | ||||
|       <EquipmentListItem | ||||
|         navigation={navigation} | ||||
|         item={item} | ||||
|         userDeviceRentDates={this.getUserDeviceRentDates(item)} | ||||
|         userDeviceRentDates={getUserDeviceRentDates(item)} | ||||
|         height={LIST_ITEM_HEIGHT} | ||||
|       /> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   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 ( | ||||
|       <View style={styles.headerContainer}> | ||||
|         <Button | ||||
|           mode="contained" | ||||
|           icon="help-circle" | ||||
|           onPress={this.showMascotDialog} | ||||
|           onPress={showMascotDialog} | ||||
|           style={GENERAL_STYLES.centerHorizontal} | ||||
|         > | ||||
|           {i18n.t('screens.equipment.mascotDialog.title')} | ||||
|         </Button> | ||||
|       </View> | ||||
|     ); | ||||
|   } | ||||
|   }; | ||||
| 
 | ||||
|   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<PropsType, StateType> { | |||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   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<DeviceType> }>('location/all') | ||||
|         requestAll() | ||||
|           .then((devicesData) => { | ||||
|             ConnectionManager.getInstance() | ||||
|               .authenticatedRequest<{ | ||||
|                 locations: Array<RentedDeviceType>; | ||||
|               }>('location/my') | ||||
|             requestOwn() | ||||
|               .then((rentsData) => { | ||||
|                 resolve({ | ||||
|                   devices: devicesData.devices, | ||||
|  | @ -175,19 +151,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> { | |||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { state } = this; | ||||
|   return ( | ||||
|     <View style={GENERAL_STYLES.flex}> | ||||
|       <WebSectionList | ||||
|           request={this.request} | ||||
|           createDataset={this.createDataset} | ||||
|           keyExtractor={this.keyExtractor} | ||||
|           renderItem={this.getRenderItem} | ||||
|           renderListHeaderComponent={() => this.getListHeader()} | ||||
|         request={request} | ||||
|         createDataset={createDataset} | ||||
|         keyExtractor={keyExtractor} | ||||
|         renderItem={getRenderItem} | ||||
|         renderListHeaderComponent={getListHeader} | ||||
|       /> | ||||
|       <MascotPopup | ||||
|           visible={state.mascotDialogVisible} | ||||
|         visible={mascotDialogVisible} | ||||
|         title={i18n.t('screens.equipment.mascotDialog.title')} | ||||
|         message={i18n.t('screens.equipment.mascotDialog.message')} | ||||
|         icon="vote" | ||||
|  | @ -195,7 +169,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> { | |||
|           cancel: { | ||||
|             message: i18n.t('screens.equipment.mascotDialog.button'), | ||||
|             icon: 'check', | ||||
|               onPress: this.hideMascotDialog, | ||||
|             onPress: hideMascotDialog, | ||||
|           }, | ||||
|         }} | ||||
|         emotion={MASCOT_STYLE.WINK} | ||||
|  | @ -203,6 +177,5 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> { | |||
|     </View> | ||||
|   ); | ||||
| } | ||||
| } | ||||
| 
 | ||||
| export default EquipmentListScreen; | ||||
|  |  | |||
|  | @ -17,21 +17,20 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<any>; | ||||
|   theme: ReactNativePaper.Theme; | ||||
| }; | ||||
| type Props = StackScreenProps<MainStackParamsList, 'equipment-rent'>; | ||||
| 
 | ||||
| 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<Props, StateType> { | ||||
|   item: DeviceType | null; | ||||
| function EquipmentRentScreen(props: Props) { | ||||
|   const theme = useTheme(); | ||||
|   const navigation = useNavigation<StackNavigationProp<any>>(); | ||||
|   const [currentError, setCurrentError] = useState<ApiRejectType>({ | ||||
|     status: REQUEST_STATUS.SUCCESS, | ||||
|   }); | ||||
|   const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({}); | ||||
|   const [dialogVisible, setDialogVisible] = useState(false); | ||||
| 
 | ||||
|   bookedDates: Array<string>; | ||||
|   const item = props.route.params.item; | ||||
| 
 | ||||
|   bookRef: { current: null | (Animatable.View & View) }; | ||||
|   const bookedDates = useRef<Array<string>>([]); | ||||
|   const canBookEquipment = useRef(false); | ||||
| 
 | ||||
|   canBookEquipment: boolean; | ||||
|   const bookRef = useRef<Animatable.View & View>(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 = {}; | ||||
|   if (item) { | ||||
|     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), | ||||
|       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', () => { | ||||
|       return () => { | ||||
|         BackHandler.removeEventListener( | ||||
|           'hardwareBackPress', | ||||
|         this.onBackButtonPressAndroid | ||||
|           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<Props, StateType> { | |||
|    * | ||||
|    * @returns {Promise<void>} | ||||
|    */ | ||||
|   onDialogAccept = (): Promise<void> => { | ||||
|   const onDialogAccept = (): Promise<void> => { | ||||
|     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,84 +240,64 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|     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; | ||||
|     } | ||||
|   const updateSelectionRange = (s: Date, e: Date) => { | ||||
|     if (item) { | ||||
|       bookedDates.current = getValidRange(s, e, item); | ||||
|     } else { | ||||
|       bookedDates.current = []; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   resetSelection() { | ||||
|     if (this.canBookEquipment) { | ||||
|       this.hideBookButton(); | ||||
|     } | ||||
|     this.canBookEquipment = false; | ||||
|     this.bookedDates = []; | ||||
|   } | ||||
|   const updateMarkedSelection = () => { | ||||
|     setMarkedDates(generateMarkedDates(true, theme, bookedDates.current)); | ||||
|   }; | ||||
| 
 | ||||
|   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()) { | ||||
|  | @ -358,7 +310,8 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|       date: getRelativeDateString(start), | ||||
|     }); | ||||
|   } | ||||
|     if (item != null) { | ||||
| 
 | ||||
|   if (item) { | ||||
|     const isAvailable = isEquipmentAvailable(item); | ||||
|     const firstAvailability = getFirstEquipmentAvailability(item); | ||||
|     return ( | ||||
|  | @ -370,9 +323,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|                 <View style={styles.titleContainer}> | ||||
|                   <Headline style={styles.title}>{item.name}</Headline> | ||||
|                   <Caption style={styles.caption}> | ||||
|                       ( | ||||
|                       {i18n.t('screens.equipment.bail', { cost: item.caution })} | ||||
|                       ) | ||||
|                     ({i18n.t('screens.equipment.bail', { cost: item.caution })}) | ||||
|                   </Caption> | ||||
|                 </View> | ||||
|               </View> | ||||
|  | @ -380,9 +331,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|               <Button | ||||
|                 icon={isAvailable ? 'check-circle-outline' : 'update'} | ||||
|                 color={ | ||||
|                     isAvailable | ||||
|                       ? props.theme.colors.success | ||||
|                       : props.theme.colors.primary | ||||
|                   isAvailable ? theme.colors.success : theme.colors.primary | ||||
|                 } | ||||
|                 mode="text" | ||||
|               > | ||||
|  | @ -390,9 +339,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|                   date: getRelativeDateString(firstAvailability), | ||||
|                 })} | ||||
|               </Button> | ||||
|                 <Subheading style={styles.subtitle}> | ||||
|                   {subHeadingText} | ||||
|                 </Subheading> | ||||
|               <Subheading style={styles.subtitle}>{subHeadingText}</Subheading> | ||||
|             </Card.Content> | ||||
|           </Card> | ||||
|           <CalendarList | ||||
|  | @ -407,28 +354,28 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|             // Enable paging on horizontal, default = false
 | ||||
|             pagingEnabled | ||||
|             // Handler which gets executed on day press. Default = undefined
 | ||||
|               onDayPress={this.selectNewDate} | ||||
|             onDayPress={selectNewDate} | ||||
|             // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
 | ||||
|             firstDay={1} | ||||
|             // Hide month navigation arrows.
 | ||||
|             hideArrows={false} | ||||
|             // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
 | ||||
|             markingType={'period'} | ||||
|               markedDates={{ ...this.lockedDates, ...state.markedDates }} | ||||
|             markedDates={{ ...lockedDates, ...markedDates }} | ||||
|             theme={{ | ||||
|                 'backgroundColor': props.theme.colors.agendaBackgroundColor, | ||||
|                 'calendarBackground': props.theme.colors.background, | ||||
|                 'textSectionTitleColor': props.theme.colors.agendaDayTextColor, | ||||
|                 'selectedDayBackgroundColor': props.theme.colors.primary, | ||||
|               'backgroundColor': theme.colors.agendaBackgroundColor, | ||||
|               'calendarBackground': theme.colors.background, | ||||
|               'textSectionTitleColor': theme.colors.agendaDayTextColor, | ||||
|               'selectedDayBackgroundColor': theme.colors.primary, | ||||
|               'selectedDayTextColor': '#ffffff', | ||||
|                 'todayTextColor': props.theme.colors.text, | ||||
|                 'dayTextColor': props.theme.colors.text, | ||||
|                 'textDisabledColor': props.theme.colors.agendaDayTextColor, | ||||
|                 'dotColor': props.theme.colors.primary, | ||||
|               'todayTextColor': theme.colors.text, | ||||
|               'dayTextColor': theme.colors.text, | ||||
|               'textDisabledColor': theme.colors.agendaDayTextColor, | ||||
|               'dotColor': theme.colors.primary, | ||||
|               'selectedDotColor': '#ffffff', | ||||
|                 'arrowColor': props.theme.colors.primary, | ||||
|                 'monthTextColor': props.theme.colors.text, | ||||
|                 'indicatorColor': props.theme.colors.primary, | ||||
|               'arrowColor': theme.colors.primary, | ||||
|               'monthTextColor': theme.colors.text, | ||||
|               'indicatorColor': theme.colors.primary, | ||||
|               'textDayFontFamily': 'monospace', | ||||
|               'textMonthFontFamily': 'monospace', | ||||
|               'textDayHeaderFontFamily': 'monospace', | ||||
|  | @ -451,29 +398,29 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|           /> | ||||
|         </CollapsibleScrollView> | ||||
|         <LoadingConfirmDialog | ||||
|             visible={state.dialogVisible} | ||||
|             onDismiss={this.onDialogDismiss} | ||||
|             onAccept={this.onDialogAccept} | ||||
|           visible={dialogVisible} | ||||
|           onDismiss={onDialogDismiss} | ||||
|           onAccept={onDialogAccept} | ||||
|           title={i18n.t('screens.equipment.dialogTitle')} | ||||
|           titleLoading={i18n.t('screens.equipment.dialogTitleLoading')} | ||||
|           message={i18n.t('screens.equipment.dialogMessage')} | ||||
|         /> | ||||
| 
 | ||||
|         <ErrorDialog | ||||
|             visible={state.errorDialogVisible} | ||||
|             onDismiss={this.onErrorDialogDismiss} | ||||
|             status={state.currentError.status} | ||||
|             code={state.currentError.code} | ||||
|           visible={currentError.status !== REQUEST_STATUS.SUCCESS} | ||||
|           onDismiss={onErrorDialogDismiss} | ||||
|           status={currentError.status} | ||||
|           code={currentError.code} | ||||
|         /> | ||||
|         <Animatable.View | ||||
|             ref={this.bookRef} | ||||
|           ref={bookRef} | ||||
|           useNativeDriver | ||||
|           style={styles.buttonContainer} | ||||
|         > | ||||
|           <Button | ||||
|             icon="bookmark-check" | ||||
|             mode="contained" | ||||
|               onPress={this.showDialog} | ||||
|             onPress={showDialog} | ||||
|             style={styles.button} | ||||
|           > | ||||
|             {i18n.t('screens.equipment.bookButton')} | ||||
|  | @ -484,6 +431,5 @@ class EquipmentRentScreen extends React.Component<Props, StateType> { | |||
|   } | ||||
|   return null; | ||||
| } | ||||
| } | ||||
| 
 | ||||
| export default withTheme(EquipmentRentScreen); | ||||
| export default EquipmentRentScreen; | ||||
|  |  | |||
|  | @ -17,19 +17,11 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<MainStackParamsList, 'login'>; | ||||
| type Props = StackScreenProps<MainStackParamsList, 'login'>; | ||||
| 
 | ||||
| type Props = LoginScreenNavigationProp & { | ||||
|   navigation: StackNavigationProp<any>; | ||||
|   theme: ReactNativePaper.Theme; | ||||
| }; | ||||
| 
 | ||||
| type StateType = { | ||||
|   email: string; | ||||
|   password: string; | ||||
|   isEmailValidated: boolean; | ||||
|   isPasswordValidated: boolean; | ||||
|   loading: boolean; | ||||
|   dialogVisible: boolean; | ||||
|   dialogError: ApiRejectType; | ||||
|   mascotDialogVisible: boolean | undefined; | ||||
| }; | ||||
| 
 | ||||
| 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', | ||||
|   }, | ||||
| function LoginScreen(props: Props) { | ||||
|   const navigation = useNavigation<StackNavigationProp<any>>(); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const [nextScreen, setNextScreen] = useState<string | undefined>(undefined); | ||||
|   const [mascotDialogVisible, setMascotDialogVisible] = useState(false); | ||||
|   const [currentError, setCurrentError] = useState<ApiRejectType>({ | ||||
|     status: REQUEST_STATUS.SUCCESS, | ||||
|   }); | ||||
|   const homeMascot = useShouldShowMascot(TabRoutes.Home); | ||||
| 
 | ||||
| class LoginScreen extends React.Component<Props, StateType> { | ||||
|   onEmailChange: (value: string) => void; | ||||
|   useFocusEffect( | ||||
|     useCallback(() => { | ||||
|       setNextScreen(props.route.params?.nextScreen); | ||||
|     }, [props.route.params]) | ||||
|   ); | ||||
| 
 | ||||
|   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<Props, StateType> { | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * 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,253 +69,37 @@ class LoginScreen extends React.Component<Props, StateType> { | |||
|    * 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 ( | ||||
|       <View> | ||||
|         <TextInput | ||||
|           label={i18n.t('screens.login.email')} | ||||
|           mode="outlined" | ||||
|           value={email} | ||||
|           onChangeText={this.onEmailChange} | ||||
|           onBlur={this.validateEmail} | ||||
|           onSubmitEditing={this.onEmailSubmit} | ||||
|           error={this.shouldShowEmailError()} | ||||
|           textContentType="emailAddress" | ||||
|           autoCapitalize="none" | ||||
|           autoCompleteType="email" | ||||
|           autoCorrect={false} | ||||
|           keyboardType="email-address" | ||||
|           returnKeyType="next" | ||||
|           secureTextEntry={false} | ||||
|         /> | ||||
|         <HelperText type="error" visible={this.shouldShowEmailError()}> | ||||
|           {i18n.t('screens.login.emailError')} | ||||
|         </HelperText> | ||||
|         <TextInput | ||||
|           ref={this.passwordInputRef} | ||||
|           label={i18n.t('screens.login.password')} | ||||
|           mode="outlined" | ||||
|           value={password} | ||||
|           onChangeText={this.onPasswordChange} | ||||
|           onBlur={this.validatePassword} | ||||
|           onSubmitEditing={this.onSubmit} | ||||
|           error={this.shouldShowPasswordError()} | ||||
|           textContentType="password" | ||||
|           autoCapitalize="none" | ||||
|           autoCompleteType="password" | ||||
|           autoCorrect={false} | ||||
|           keyboardType="default" | ||||
|           returnKeyType="done" | ||||
|           secureTextEntry | ||||
|         /> | ||||
|         <HelperText type="error" visible={this.shouldShowPasswordError()}> | ||||
|           {i18n.t('screens.login.passwordError')} | ||||
|         </HelperText> | ||||
|       </View> | ||||
|     ); | ||||
|   } | ||||
|   const hideMascotDialog = () => setMascotDialogVisible(true); | ||||
| 
 | ||||
|   /** | ||||
|    * Gets the card containing the input form | ||||
|    * @returns {*} | ||||
|    */ | ||||
|   getMainCard() { | ||||
|     const { props, state } = this; | ||||
|     return ( | ||||
|       <View style={styles.card}> | ||||
|         <Card.Title | ||||
|           title={i18n.t('screens.login.title')} | ||||
|           titleStyle={styles.text} | ||||
|           subtitle={i18n.t('screens.login.subtitle')} | ||||
|           subtitleStyle={styles.text} | ||||
|           left={({ size }) => ( | ||||
|             <Image | ||||
|               source={ICON_AMICALE} | ||||
|               style={{ | ||||
|                 width: size, | ||||
|                 height: size, | ||||
|               }} | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|         <Card.Content> | ||||
|           {this.getFormInput()} | ||||
|           <Card.Actions style={styles.buttonContainer}> | ||||
|             <Button | ||||
|               icon="lock-question" | ||||
|               mode="contained" | ||||
|               onPress={this.onResetPasswordClick} | ||||
|               color={props.theme.colors.warning} | ||||
|               style={styles.lockButton} | ||||
|             > | ||||
|               {i18n.t('screens.login.resetPassword')} | ||||
|             </Button> | ||||
|             <Button | ||||
|               icon="send" | ||||
|               mode="contained" | ||||
|               disabled={!this.shouldEnableLogin()} | ||||
|               loading={state.loading} | ||||
|               onPress={this.onSubmit} | ||||
|               style={styles.sendButton} | ||||
|             > | ||||
|               {i18n.t('screens.login.title')} | ||||
|             </Button> | ||||
|           </Card.Actions> | ||||
|           <Card.Actions> | ||||
|             <Button | ||||
|               icon="help-circle" | ||||
|               mode="contained" | ||||
|               onPress={this.showMascotDialog} | ||||
|               style={GENERAL_STYLES.centerHorizontal} | ||||
|             > | ||||
|               {i18n.t('screens.login.mascotDialog.title')} | ||||
|             </Button> | ||||
|           </Card.Actions> | ||||
|         </Card.Content> | ||||
|       </View> | ||||
|     ); | ||||
|   } | ||||
|   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 ( | ||||
|     <LinearGradient | ||||
|       style={GENERAL_STYLES.flex} | ||||
|  | @ -438,7 +115,14 @@ class LoginScreen extends React.Component<Props, StateType> { | |||
|         keyboardVerticalOffset={100} | ||||
|       > | ||||
|         <CollapsibleScrollView headerColors={'transparent'}> | ||||
|             <View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View> | ||||
|           <View style={GENERAL_STYLES.flex}> | ||||
|             <LoginForm | ||||
|               loading={loading} | ||||
|               onSubmit={onSubmit} | ||||
|               onResetPasswordPress={onResetPasswordClick} | ||||
|               onHelpPress={showMascotDialog} | ||||
|             /> | ||||
|           </View> | ||||
|           <MascotPopup | ||||
|             visible={mascotDialogVisible} | ||||
|             title={i18n.t('screens.login.mascotDialog.title')} | ||||
|  | @ -448,22 +132,21 @@ class LoginScreen extends React.Component<Props, StateType> { | |||
|               cancel: { | ||||
|                 message: i18n.t('screens.login.mascotDialog.button'), | ||||
|                 icon: 'check', | ||||
|                   onPress: this.hideMascotDialog, | ||||
|                 onPress: hideMascotDialog, | ||||
|               }, | ||||
|             }} | ||||
|             emotion={MASCOT_STYLE.NORMAL} | ||||
|           /> | ||||
|           <ErrorDialog | ||||
|               visible={dialogVisible} | ||||
|               onDismiss={this.hideErrorDialog} | ||||
|               status={dialogError.status} | ||||
|               code={dialogError.code} | ||||
|             visible={currentError.status !== REQUEST_STATUS.SUCCESS} | ||||
|             onDismiss={hideErrorDialog} | ||||
|             status={currentError.status} | ||||
|             code={currentError.code} | ||||
|           /> | ||||
|         </CollapsibleScrollView> | ||||
|       </KeyboardAvoidingView> | ||||
|     </LinearGradient> | ||||
|   ); | ||||
| } | ||||
| } | ||||
| 
 | ||||
| export default withTheme(LoginScreen); | ||||
| export default LoginScreen; | ||||
|  |  | |||
|  | @ -17,52 +17,29 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<any>; | ||||
|   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<ClubType>; | ||||
|   clubs: Array<ProfileClubType>; | ||||
| }; | ||||
| 
 | ||||
| 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<ProfileDataType>('user/profile'); | ||||
| 
 | ||||
| class ProfileScreen extends React.Component<PropsType, StateType> { | ||||
|   data: ProfileDataType | undefined; | ||||
| 
 | ||||
|   flatListData: Array<{ id: string }>; | ||||
| 
 | ||||
|   amicaleDataset: Array<ServiceItemType>; | ||||
| 
 | ||||
|   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; | ||||
|     navigation.setOptions({ | ||||
|       headerRight: this.getHeaderButton, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets the logout header button | ||||
|    * | ||||
|    * @returns {*} | ||||
|    */ | ||||
|   getHeaderButton = () => ( | ||||
|   useLayoutEffect(() => { | ||||
|     const getHeaderButton = () => ( | ||||
|       <MaterialHeaderButtons> | ||||
|         <Item | ||||
|         title="logout" | ||||
|         iconName="logout" | ||||
|         onPress={this.showDisconnectDialog} | ||||
|           title={'logout'} | ||||
|           iconName={'logout'} | ||||
|           onPress={showDisconnectDialog} | ||||
|         /> | ||||
|       </MaterialHeaderButtons> | ||||
|     ); | ||||
|     navigation.setOptions({ | ||||
|       headerRight: getHeaderButton, | ||||
|     }); | ||||
|   }, [navigation]); | ||||
| 
 | ||||
|   /** | ||||
|    * 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: () => <ProfileWelcomeCard firstname={data?.first_name} />, | ||||
|             }); | ||||
|             break; | ||||
|           case 1: | ||||
|             flatListData.push({ | ||||
|               id: i.toString(), | ||||
|               render: () => <ProfilePersonalCard profile={data} />, | ||||
|             }); | ||||
|             break; | ||||
|           case 2: | ||||
|             flatListData.push({ | ||||
|               id: i.toString(), | ||||
|               render: () => <ProfileClubCard clubs={data?.clubs} />, | ||||
|             }); | ||||
|             break; | ||||
|           default: | ||||
|             flatListData.push({ | ||||
|               id: i.toString(), | ||||
|               render: () => <ProfileMembershipCard valid={data?.validity} />, | ||||
|             }); | ||||
|         } | ||||
|       } | ||||
|       return ( | ||||
|         <View style={GENERAL_STYLES.flex}> | ||||
|           <CollapsibleFlatList | ||||
|             renderItem={this.getRenderItem} | ||||
|             data={this.flatListData} | ||||
|           /> | ||||
|           <CollapsibleFlatList renderItem={getRenderItem} data={flatListData} /> | ||||
|           <LogoutDialog | ||||
|             visible={dialogVisible} | ||||
|             onDismiss={this.hideDisconnectDialog} | ||||
|             onDismiss={hideDisconnectDialog} | ||||
|           /> | ||||
|         </View> | ||||
|       ); | ||||
|  | @ -160,346 +118,17 @@ class ProfileScreen extends React.Component<PropsType, StateType> { | |||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   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 <CardList dataset={this.amicaleDataset} isHorizontal />; | ||||
|   const showDisconnectDialog = () => setDialogVisible(true); | ||||
| 
 | ||||
|   const hideDisconnectDialog = () => setDialogVisible(false); | ||||
| 
 | ||||
|   return <RequestScreen request={request} render={getScreen} />; | ||||
| } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets a card welcoming the user to his account | ||||
|    * | ||||
|    * @returns {*} | ||||
|    */ | ||||
|   getWelcomeCard() { | ||||
|     const { navigation } = this.props; | ||||
|     return ( | ||||
|       <Card style={styles.card}> | ||||
|         <Card.Title | ||||
|           title={i18n.t('screens.profile.welcomeTitle', { | ||||
|             name: this.data?.first_name, | ||||
|           })} | ||||
|           left={() => ( | ||||
|             <Mascot | ||||
|               style={styles.mascot} | ||||
|               emotion={MASCOT_STYLE.COOL} | ||||
|               animated | ||||
|               entryAnimation={{ | ||||
|                 animation: 'bounceIn', | ||||
|                 duration: 1000, | ||||
|               }} | ||||
|             /> | ||||
|           )} | ||||
|           titleStyle={styles.title} | ||||
|         /> | ||||
|         <Card.Content> | ||||
|           <Divider /> | ||||
|           <Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph> | ||||
|           {this.getServicesList()} | ||||
|           <Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph> | ||||
|           <Divider /> | ||||
|           <Card.Actions> | ||||
|             <Button | ||||
|               icon="bug" | ||||
|               mode="contained" | ||||
|               onPress={() => { | ||||
|                 navigation.navigate('feedback'); | ||||
|               }} | ||||
|               style={styles.editButton} | ||||
|             > | ||||
|               {i18n.t('screens.feedback.homeButtonTitle')} | ||||
|             </Button> | ||||
|           </Card.Actions> | ||||
|         </Card.Content> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * 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 ( | ||||
|       <List.Item | ||||
|         title={title} | ||||
|         description={subtitle} | ||||
|         left={(props) => ( | ||||
|           <List.Icon | ||||
|             style={props.style} | ||||
|             icon={icon} | ||||
|             color={field != null ? props.color : theme.colors.textDisabled} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets a card containing user personal information | ||||
|    * | ||||
|    * @return {*} | ||||
|    */ | ||||
|   getPersonalCard() { | ||||
|     const { theme, navigation } = this.props; | ||||
|     return ( | ||||
|       <Card style={styles.card}> | ||||
|         <Card.Title | ||||
|           title={`${this.data?.first_name} ${this.data?.last_name}`} | ||||
|           subtitle={this.data?.email} | ||||
|           left={(iconProps) => ( | ||||
|             <Avatar.Icon | ||||
|               size={iconProps.size} | ||||
|               icon="account" | ||||
|               color={theme.colors.primary} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|         <Card.Content> | ||||
|           <Divider /> | ||||
|           <List.Section> | ||||
|             <List.Subheader> | ||||
|               {i18n.t('screens.profile.personalInformation')} | ||||
|             </List.Subheader> | ||||
|             {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')} | ||||
|           </List.Section> | ||||
|           <Divider /> | ||||
|           <Card.Actions> | ||||
|             <Button | ||||
|               icon="account-edit" | ||||
|               mode="contained" | ||||
|               onPress={() => { | ||||
|                 navigation.navigate('website', { | ||||
|                   host: Urls.websites.amicale, | ||||
|                   path: this.data?.link, | ||||
|                   title: i18n.t('screens.websites.amicale'), | ||||
|                 }); | ||||
|               }} | ||||
|               style={styles.editButton} | ||||
|             > | ||||
|               {i18n.t('screens.profile.editInformation')} | ||||
|             </Button> | ||||
|           </Card.Actions> | ||||
|         </Card.Content> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets a cars containing clubs the user is part of | ||||
|    * | ||||
|    * @return {*} | ||||
|    */ | ||||
|   getClubCard() { | ||||
|     const { theme } = this.props; | ||||
|     return ( | ||||
|       <Card style={styles.card}> | ||||
|         <Card.Title | ||||
|           title={i18n.t('screens.profile.clubs')} | ||||
|           subtitle={i18n.t('screens.profile.clubsSubtitle')} | ||||
|           left={(iconProps) => ( | ||||
|             <Avatar.Icon | ||||
|               size={iconProps.size} | ||||
|               icon="account-group" | ||||
|               color={theme.colors.primary} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|         <Card.Content> | ||||
|           <Divider /> | ||||
|           {this.getClubList(this.data?.clubs)} | ||||
|         </Card.Content> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets a card showing if the user has payed his membership | ||||
|    * | ||||
|    * @return {*} | ||||
|    */ | ||||
|   getMembershipCar() { | ||||
|     const { theme } = this.props; | ||||
|     return ( | ||||
|       <Card style={styles.card}> | ||||
|         <Card.Title | ||||
|           title={i18n.t('screens.profile.membership')} | ||||
|           subtitle={i18n.t('screens.profile.membershipSubtitle')} | ||||
|           left={(iconProps) => ( | ||||
|             <Avatar.Icon | ||||
|               size={iconProps.size} | ||||
|               icon="credit-card" | ||||
|               color={theme.colors.primary} | ||||
|               style={styles.icon} | ||||
|             /> | ||||
|           )} | ||||
|         /> | ||||
|         <Card.Content> | ||||
|           <List.Section> | ||||
|             {this.getMembershipItem(this.data?.validity === true)} | ||||
|           </List.Section> | ||||
|         </Card.Content> | ||||
|       </Card> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Gets the item showing if the user has payed his membership | ||||
|    * | ||||
|    * @return {*} | ||||
|    */ | ||||
|   getMembershipItem(state: boolean) { | ||||
|     const { theme } = this.props; | ||||
|     return ( | ||||
|       <List.Item | ||||
|         title={ | ||||
|           state | ||||
|             ? i18n.t('screens.profile.membershipPayed') | ||||
|             : i18n.t('screens.profile.membershipNotPayed') | ||||
|         } | ||||
|         left={(props) => ( | ||||
|           <List.Icon | ||||
|             style={props.style} | ||||
|             color={state ? theme.colors.success : theme.colors.danger} | ||||
|             icon={state ? 'check' : 'close'} | ||||
|           /> | ||||
|         )} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * 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; | ||||
|       }; | ||||
|     }) => ( | ||||
|       <List.Icon color={props.color} style={props.style} icon="chevron-right" /> | ||||
|     ); | ||||
|     if (item.is_manager) { | ||||
|       description = i18n.t('screens.profile.isManager'); | ||||
|       icon = (props) => ( | ||||
|         <List.Icon | ||||
|           style={props.style} | ||||
|           icon="star" | ||||
|           color={theme.colors.primary} | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
|     return ( | ||||
|       <List.Item | ||||
|         title={item.name} | ||||
|         description={description} | ||||
|         left={icon} | ||||
|         onPress={onPress} | ||||
|       /> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * Renders the list of clubs the user is part of | ||||
|    * | ||||
|    * @param list The club list | ||||
|    * @return {*} | ||||
|    */ | ||||
|   getClubList(list: Array<ClubType> | undefined) { | ||||
|     if (!list) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     list.sort(this.sortClubList); | ||||
|     return ( | ||||
|       <FlatList | ||||
|         renderItem={this.getClubListItem} | ||||
|         keyExtractor={this.clubKeyExtractor} | ||||
|         data={list} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   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 ( | ||||
|       <RequestScreen<ProfileDataType> | ||||
|         request={() => | ||||
|           ConnectionManager.getInstance().authenticatedRequest('user/profile') | ||||
|         } | ||||
|         render={this.getScreen} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default withTheme(ProfileScreen); | ||||
| export default ProfileScreen; | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ | |||
|  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 | ||||
|  */ | ||||
| 
 | ||||
| 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<VoteTeamType>; | ||||
|   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<PropsType, StateType> { | ||||
|   teams: Array<VoteTeamType>; | ||||
| 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<VoteDatesStringType>( | ||||
|     'elections/dates' | ||||
|   ); | ||||
|   const teamsRequest = useAuthenticatedRequest<TeamResponseType>( | ||||
|     '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<PropsType, StateType> { | |||
|    * @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 ( | ||||
|         <View> | ||||
|           <Button | ||||
|             mode="contained" | ||||
|             icon="help-circle" | ||||
|             onPress={this.showMascotDialog} | ||||
|             onPress={showMascotDialog} | ||||
|             style={styles.button} | ||||
|           > | ||||
|             {i18n.t('screens.vote.mascotDialog.title')} | ||||
|  | @ -196,10 +181,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> { | |||
|         </View> | ||||
|       ); | ||||
|     } | ||||
|     return this.getContent(); | ||||
|     if (item.data) { | ||||
|       return getContent(item.data); | ||||
|     } else { | ||||
|       return <View />; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   createDataset = ( | ||||
|   const createDataset = ( | ||||
|     data: ResponseType | undefined, | ||||
|     _loading: boolean, | ||||
|     _lastRefreshDate: Date | undefined, | ||||
|  | @ -207,157 +196,158 @@ export default class VoteScreen extends React.Component<PropsType, StateType> { | |||
|   ) => { | ||||
|     // 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 <VoteNotAvailable />; | ||||
|   } | ||||
| 
 | ||||
|   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 ( | ||||
|       <VoteSelect | ||||
|         teams={this.teams} | ||||
|         onVoteSuccess={this.onVoteSuccess} | ||||
|         onVoteError={this.refreshData} | ||||
|         teams={data.teams} | ||||
|         onVoteSuccess={onVoteSuccess} | ||||
|         onVoteError={() => { | ||||
|           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 ( | ||||
|         <VoteResults | ||||
|           teams={this.teams} | ||||
|           dateEnd={this.getDateString( | ||||
|             this.dates.date_result_end, | ||||
|             this.datesString.date_result_end | ||||
|           teams={data.teams} | ||||
|           dateEnd={getDateString( | ||||
|             data.dates.date_result_end, | ||||
|             data.datesString.date_result_end | ||||
|           )} | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
|     return <VoteNotAvailable />; | ||||
|   } | ||||
| 
 | ||||
|   }; | ||||
|   /** | ||||
|    * Vote will open shortly | ||||
|    */ | ||||
|   getTeaseVoteCard() { | ||||
|     if (this.dates != null && this.datesString != null) { | ||||
|   const getTeaseVoteCard = (data: FlatlistType) => { | ||||
|     if (data.dates != null && data.datesString != null) { | ||||
|       return ( | ||||
|         <VoteTease | ||||
|           startDate={this.getDateString( | ||||
|             this.dates.date_begin, | ||||
|             this.datesString.date_begin | ||||
|           startDate={getDateString( | ||||
|             data.dates.date_begin, | ||||
|             data.datesString.date_begin | ||||
|           )} | ||||
|         /> | ||||
|       ); | ||||
|     } | ||||
|     return <VoteNotAvailable />; | ||||
|   } | ||||
| 
 | ||||
|   }; | ||||
|   /** | ||||
|    * 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 ( | ||||
|       <VoteWait | ||||
|         startDate={startDate} | ||||
|         hasVoted={this.hasVoted || state.hasVoted} | ||||
|         justVoted={state.hasVoted} | ||||
|         isVoteRunning={this.isVoteRunning()} | ||||
|         hasVoted={data.hasVoted} | ||||
|         justVoted={hasVoted} | ||||
|         isVoteRunning={isVoteRunning()} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   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<PropsType, StateType> { | |||
|         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<VoteDatesStringType>('elections/dates') | ||||
|       datesRequest() | ||||
|         .then((datesData) => { | ||||
|           ConnectionManager.getInstance() | ||||
|             .authenticatedRequest<TeamResponseType>('elections/teams') | ||||
|           teamsRequest() | ||||
|             .then((teamsData) => { | ||||
|               resolve({ | ||||
|                 dates: datesData, | ||||
|  | @ -405,25 +393,16 @@ export default class VoteScreen extends React.Component<PropsType, StateType> { | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    * 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 ( | ||||
|     <View style={GENERAL_STYLES.flex}> | ||||
|       <WebSectionList | ||||
|           request={this.request} | ||||
|           createDataset={this.createDataset} | ||||
|           extraData={state.hasVoted.toString()} | ||||
|           renderItem={this.getMainRenderItem} | ||||
|         request={request} | ||||
|         createDataset={createDataset} | ||||
|         extraData={hasVoted.toString()} | ||||
|         renderItem={getMainRenderItem} | ||||
|       /> | ||||
|       <MascotPopup | ||||
|           visible={state.mascotDialogVisible} | ||||
|         visible={mascotDialogVisible} | ||||
|         title={i18n.t('screens.vote.mascotDialog.title')} | ||||
|         message={i18n.t('screens.vote.mascotDialog.message')} | ||||
|         icon="vote" | ||||
|  | @ -431,7 +410,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> { | |||
|           cancel: { | ||||
|             message: i18n.t('screens.vote.mascotDialog.button'), | ||||
|             icon: 'check', | ||||
|               onPress: this.hideMascotDialog, | ||||
|             onPress: hideMascotDialog, | ||||
|           }, | ||||
|         }} | ||||
|         emotion={MASCOT_STYLE.CUTE} | ||||
|  | @ -439,4 +418,3 @@ export default class VoteScreen extends React.Component<PropsType, StateType> { | |||
|     </View> | ||||
|   ); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -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<AnimatedFAB>(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]) | ||||
|   ); | ||||
|  |  | |||
|  | @ -80,7 +80,8 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean { | |||
| export async function apiRequest<T>( | ||||
|   path: string, | ||||
|   method: string, | ||||
|   params?: object | ||||
|   params?: object, | ||||
|   token?: string | ||||
| ): Promise<T> { | ||||
|   return new Promise( | ||||
|     (resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => { | ||||
|  | @ -88,7 +89,9 @@ export async function apiRequest<T>( | |||
|       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<T>( | |||
|   ); | ||||
| } | ||||
| 
 | ||||
| export async function connectToAmicale(email: string, password: string) { | ||||
|   return new Promise( | ||||
|     ( | ||||
|       resolve: (token: string) => void, | ||||
|       reject: (error: ApiRejectType) => void | ||||
|     ) => { | ||||
|       const data = { | ||||
|         email, | ||||
|         password, | ||||
|       }; | ||||
|       apiRequest<ApiDataLoginType>('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. | ||||
|  * | ||||
|  |  | |||
							
								
								
									
										46
									
								
								src/utils/loginToken.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/utils/loginToken.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| import * as Keychain from 'react-native-keychain'; | ||||
| 
 | ||||
| /** | ||||
|  * Tries to recover login token from the secure keychain | ||||
|  * | ||||
|  * @returns Promise<string | undefined> | ||||
|  */ | ||||
| export async function retrieveLoginToken(): Promise<string | undefined> { | ||||
|   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<void> | ||||
|  */ | ||||
| export async function saveLoginToken( | ||||
|   email: string, | ||||
|   token: string | ||||
| ): Promise<void> { | ||||
|   return new Promise((resolve: () => void, reject: () => void) => { | ||||
|     Keychain.setGenericPassword(email, token).then(resolve).catch(reject); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Deletes the login token from the keychain | ||||
|  * | ||||
|  * @returns Promise<void> | ||||
|  */ | ||||
| export async function deleteLoginToken(): Promise<void> { | ||||
|   return new Promise((resolve: () => void, reject: () => void) => { | ||||
|     Keychain.resetGenericPassword().then(resolve).catch(reject); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										11
									
								
								src/utils/logout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/utils/logout.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
| }; | ||||
		Loading…
	
		Reference in a new issue