Browse Source

convert connection manager to context

Arnaud Vergnet 2 years ago
parent
commit
541c002558

+ 16
- 9
App.tsx View File

21
 import { LogBox, Platform } from 'react-native';
21
 import { LogBox, Platform } from 'react-native';
22
 import { setSafeBounceHeight } from 'react-navigation-collapsible';
22
 import { setSafeBounceHeight } from 'react-navigation-collapsible';
23
 import SplashScreen from 'react-native-splash-screen';
23
 import SplashScreen from 'react-native-splash-screen';
24
-import ConnectionManager from './src/managers/ConnectionManager';
25
 import type { ParsedUrlDataType } from './src/utils/URLHandler';
24
 import type { ParsedUrlDataType } from './src/utils/URLHandler';
26
 import URLHandler from './src/utils/URLHandler';
25
 import URLHandler from './src/utils/URLHandler';
27
 import initLocales from './src/utils/Locales';
26
 import initLocales from './src/utils/Locales';
48
   ProxiwashPreferencesProvider,
47
   ProxiwashPreferencesProvider,
49
 } from './src/components/providers/PreferencesProvider';
48
 } from './src/components/providers/PreferencesProvider';
50
 import MainApp from './src/screens/MainApp';
49
 import MainApp from './src/screens/MainApp';
50
+import LoginProvider from './src/components/providers/LoginProvider';
51
+import { retrieveLoginToken } from './src/utils/loginToken';
51
 
52
 
52
 // Native optimizations https://reactnavigation.org/docs/react-native-screens
53
 // Native optimizations https://reactnavigation.org/docs/react-native-screens
53
 // Crashes app when navigating away from webview on android 9+
54
 // Crashes app when navigating away from webview on android 9+
67
     proxiwash: ProxiwashPreferencesType;
68
     proxiwash: ProxiwashPreferencesType;
68
     mascot: MascotPreferencesType;
69
     mascot: MascotPreferencesType;
69
   };
70
   };
71
+  loginToken?: string;
70
 };
72
 };
71
 
73
 
72
 export default class App extends React.Component<{}, StateType> {
74
 export default class App extends React.Component<{}, StateType> {
88
         proxiwash: defaultProxiwashPreferences,
90
         proxiwash: defaultProxiwashPreferences,
89
         mascot: defaultMascotPreferences,
91
         mascot: defaultMascotPreferences,
90
       },
92
       },
93
+      loginToken: undefined,
91
     };
94
     };
92
     initLocales();
95
     initLocales();
93
     this.navigatorRef = React.createRef();
96
     this.navigatorRef = React.createRef();
136
       | PlanexPreferencesType
139
       | PlanexPreferencesType
137
       | ProxiwashPreferencesType
140
       | ProxiwashPreferencesType
138
       | MascotPreferencesType
141
       | MascotPreferencesType
139
-      | void
142
+      | string
143
+      | undefined
140
     >
144
     >
141
   ) => {
145
   ) => {
142
-    const [general, planex, proxiwash, mascot] = values;
146
+    const [general, planex, proxiwash, mascot, token] = values;
143
     this.setState({
147
     this.setState({
144
       isLoading: false,
148
       isLoading: false,
145
       initialPreferences: {
149
       initialPreferences: {
148
         proxiwash: proxiwash as ProxiwashPreferencesType,
152
         proxiwash: proxiwash as ProxiwashPreferencesType,
149
         mascot: mascot as MascotPreferencesType,
153
         mascot: mascot as MascotPreferencesType,
150
       },
154
       },
155
+      loginToken: token as string | undefined,
151
     });
156
     });
152
     SplashScreen.hide();
157
     SplashScreen.hide();
153
   };
158
   };
175
         Object.values(MascotPreferenceKeys),
180
         Object.values(MascotPreferenceKeys),
176
         defaultMascotPreferences
181
         defaultMascotPreferences
177
       ),
182
       ),
178
-      ConnectionManager.getInstance().recoverLogin(),
183
+      retrieveLoginToken(),
179
     ])
184
     ])
180
       .then(this.onLoadFinished)
185
       .then(this.onLoadFinished)
181
       .catch(this.onLoadFinished);
186
       .catch(this.onLoadFinished);
202
             <MascotPreferencesProvider
207
             <MascotPreferencesProvider
203
               initialPreferences={this.state.initialPreferences.mascot}
208
               initialPreferences={this.state.initialPreferences.mascot}
204
             >
209
             >
205
-              <MainApp
206
-                ref={this.navigatorRef}
207
-                defaultHomeData={this.defaultHomeData}
208
-                defaultHomeRoute={this.defaultHomeRoute}
209
-              />
210
+              <LoginProvider initialToken={this.state.loginToken}>
211
+                <MainApp
212
+                  ref={this.navigatorRef}
213
+                  defaultHomeData={this.defaultHomeData}
214
+                  defaultHomeRoute={this.defaultHomeRoute}
215
+                />
216
+              </LoginProvider>
210
             </MascotPreferencesProvider>
217
             </MascotPreferencesProvider>
211
           </ProxiwashPreferencesProvider>
218
           </ProxiwashPreferencesProvider>
212
         </PlanexPreferencesProvider>
219
         </PlanexPreferencesProvider>

+ 231
- 0
src/components/Amicale/Login/LoginForm.tsx View File

1
+import React, { useRef, useState } from 'react';
2
+import {
3
+  Image,
4
+  StyleSheet,
5
+  View,
6
+  TextInput as RNTextInput,
7
+} from 'react-native';
8
+import {
9
+  Button,
10
+  Card,
11
+  HelperText,
12
+  TextInput,
13
+  useTheme,
14
+} from 'react-native-paper';
15
+import i18n from 'i18n-js';
16
+import GENERAL_STYLES from '../../../constants/Styles';
17
+
18
+type Props = {
19
+  loading: boolean;
20
+  onSubmit: (email: string, password: string) => void;
21
+  onHelpPress: () => void;
22
+  onResetPasswordPress: () => void;
23
+};
24
+
25
+const ICON_AMICALE = require('../../../assets/amicale.png');
26
+
27
+const styles = StyleSheet.create({
28
+  card: {
29
+    marginTop: 'auto',
30
+    marginBottom: 'auto',
31
+  },
32
+  header: {
33
+    fontSize: 36,
34
+    marginBottom: 48,
35
+  },
36
+  text: {
37
+    color: '#ffffff',
38
+  },
39
+  buttonContainer: {
40
+    flexWrap: 'wrap',
41
+  },
42
+  lockButton: {
43
+    marginRight: 'auto',
44
+    marginBottom: 20,
45
+  },
46
+  sendButton: {
47
+    marginLeft: 'auto',
48
+  },
49
+});
50
+
51
+const emailRegex = /^.+@.+\..+$/;
52
+
53
+/**
54
+ * Checks if the entered email is valid (matches the regex)
55
+ *
56
+ * @returns {boolean}
57
+ */
58
+function isEmailValid(email: string): boolean {
59
+  return emailRegex.test(email);
60
+}
61
+
62
+/**
63
+ * Checks if the user has entered a password
64
+ *
65
+ * @returns {boolean}
66
+ */
67
+function isPasswordValid(password: string): boolean {
68
+  return password !== '';
69
+}
70
+
71
+export default function LoginForm(props: Props) {
72
+  const theme = useTheme();
73
+  const [email, setEmail] = useState('');
74
+  const [password, setPassword] = useState('');
75
+  const [isEmailValidated, setIsEmailValidated] = useState(false);
76
+  const [isPasswordValidated, setIsPasswordValidated] = useState(false);
77
+  const passwordRef = useRef<RNTextInput>(null);
78
+  /**
79
+   * Checks if we should tell the user his email is invalid.
80
+   * We should only show this if his email is invalid and has been checked when un-focusing the input
81
+   *
82
+   * @returns {boolean|boolean}
83
+   */
84
+  const shouldShowEmailError = () => {
85
+    return isEmailValidated && !isEmailValid(email);
86
+  };
87
+
88
+  /**
89
+   * Checks if we should tell the user his password is invalid.
90
+   * We should only show this if his password is invalid and has been checked when un-focusing the input
91
+   *
92
+   * @returns {boolean|boolean}
93
+   */
94
+  const shouldShowPasswordError = () => {
95
+    return isPasswordValidated && !isPasswordValid(password);
96
+  };
97
+
98
+  const onEmailSubmit = () => {
99
+    if (passwordRef.current) {
100
+      passwordRef.current.focus();
101
+    }
102
+  };
103
+
104
+  /**
105
+   * The user has unfocused the input, his email is ready to be validated
106
+   */
107
+  const validateEmail = () => setIsEmailValidated(true);
108
+
109
+  /**
110
+   * The user has unfocused the input, his password is ready to be validated
111
+   */
112
+  const validatePassword = () => setIsPasswordValidated(true);
113
+
114
+  const onEmailChange = (value: string) => {
115
+    if (isEmailValidated) {
116
+      setIsEmailValidated(false);
117
+    }
118
+    setEmail(value);
119
+  };
120
+
121
+  const onPasswordChange = (value: string) => {
122
+    if (isPasswordValidated) {
123
+      setIsPasswordValidated(false);
124
+    }
125
+    setPassword(value);
126
+  };
127
+
128
+  const shouldEnableLogin = () => {
129
+    return isEmailValid(email) && isPasswordValid(password) && !props.loading;
130
+  };
131
+
132
+  const onSubmit = () => {
133
+    if (shouldEnableLogin()) {
134
+      props.onSubmit(email, password);
135
+    }
136
+  };
137
+
138
+  return (
139
+    <View style={styles.card}>
140
+      <Card.Title
141
+        title={i18n.t('screens.login.title')}
142
+        titleStyle={styles.text}
143
+        subtitle={i18n.t('screens.login.subtitle')}
144
+        subtitleStyle={styles.text}
145
+        left={({ size }) => (
146
+          <Image
147
+            source={ICON_AMICALE}
148
+            style={{
149
+              width: size,
150
+              height: size,
151
+            }}
152
+          />
153
+        )}
154
+      />
155
+      <Card.Content>
156
+        <View>
157
+          <TextInput
158
+            label={i18n.t('screens.login.email')}
159
+            mode={'outlined'}
160
+            value={email}
161
+            onChangeText={onEmailChange}
162
+            onBlur={validateEmail}
163
+            onSubmitEditing={onEmailSubmit}
164
+            error={shouldShowEmailError()}
165
+            textContentType={'emailAddress'}
166
+            autoCapitalize={'none'}
167
+            autoCompleteType={'email'}
168
+            autoCorrect={false}
169
+            keyboardType={'email-address'}
170
+            returnKeyType={'next'}
171
+            secureTextEntry={false}
172
+          />
173
+          <HelperText type={'error'} visible={shouldShowEmailError()}>
174
+            {i18n.t('screens.login.emailError')}
175
+          </HelperText>
176
+          <TextInput
177
+            ref={passwordRef}
178
+            label={i18n.t('screens.login.password')}
179
+            mode={'outlined'}
180
+            value={password}
181
+            onChangeText={onPasswordChange}
182
+            onBlur={validatePassword}
183
+            onSubmitEditing={onSubmit}
184
+            error={shouldShowPasswordError()}
185
+            textContentType={'password'}
186
+            autoCapitalize={'none'}
187
+            autoCompleteType={'password'}
188
+            autoCorrect={false}
189
+            keyboardType={'default'}
190
+            returnKeyType={'done'}
191
+            secureTextEntry={true}
192
+          />
193
+          <HelperText type={'error'} visible={shouldShowPasswordError()}>
194
+            {i18n.t('screens.login.passwordError')}
195
+          </HelperText>
196
+        </View>
197
+        <Card.Actions style={styles.buttonContainer}>
198
+          <Button
199
+            icon="lock-question"
200
+            mode="contained"
201
+            onPress={props.onResetPasswordPress}
202
+            color={theme.colors.warning}
203
+            style={styles.lockButton}
204
+          >
205
+            {i18n.t('screens.login.resetPassword')}
206
+          </Button>
207
+          <Button
208
+            icon="send"
209
+            mode="contained"
210
+            disabled={!shouldEnableLogin()}
211
+            loading={props.loading}
212
+            onPress={onSubmit}
213
+            style={styles.sendButton}
214
+          >
215
+            {i18n.t('screens.login.title')}
216
+          </Button>
217
+        </Card.Actions>
218
+        <Card.Actions>
219
+          <Button
220
+            icon="help-circle"
221
+            mode="contained"
222
+            onPress={props.onHelpPress}
223
+            style={GENERAL_STYLES.centerHorizontal}
224
+          >
225
+            {i18n.t('screens.login.mascotDialog.title')}
226
+          </Button>
227
+        </Card.Actions>
228
+      </Card.Content>
229
+    </View>
230
+  );
231
+}

+ 5
- 13
src/components/Amicale/LogoutDialog.tsx View File

20
 import * as React from 'react';
20
 import * as React from 'react';
21
 import i18n from 'i18n-js';
21
 import i18n from 'i18n-js';
22
 import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
22
 import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
23
-import ConnectionManager from '../../managers/ConnectionManager';
24
-import { useNavigation } from '@react-navigation/native';
23
+import { useLogout } from '../../utils/logout';
25
 
24
 
26
 type PropsType = {
25
 type PropsType = {
27
   visible: boolean;
26
   visible: boolean;
29
 };
28
 };
30
 
29
 
31
 function LogoutDialog(props: PropsType) {
30
 function LogoutDialog(props: PropsType) {
32
-  const navigation = useNavigation();
31
+  const onLogout = useLogout();
32
+  // Use a loading dialog as it can take some time to update the context
33
   const onClickAccept = async (): Promise<void> => {
33
   const onClickAccept = async (): Promise<void> => {
34
     return new Promise((resolve: () => void) => {
34
     return new Promise((resolve: () => void) => {
35
-      ConnectionManager.getInstance()
36
-        .disconnect()
37
-        .then(() => {
38
-          navigation.reset({
39
-            index: 0,
40
-            routes: [{ name: 'main' }],
41
-          });
42
-          props.onDismiss();
43
-          resolve();
44
-        });
35
+      onLogout();
36
+      resolve();
45
     });
37
     });
46
   };
38
   };
47
 
39
 

+ 100
- 0
src/components/Amicale/Profile/ProfileClubCard.tsx View File

1
+import React from 'react';
2
+import { Card, Avatar, Divider, useTheme, List } from 'react-native-paper';
3
+import i18n from 'i18n-js';
4
+import { FlatList, StyleSheet } from 'react-native';
5
+import { ProfileClubType } from '../../../screens/Amicale/ProfileScreen';
6
+import { useNavigation } from '@react-navigation/core';
7
+
8
+type Props = {
9
+  clubs?: Array<ProfileClubType>;
10
+};
11
+
12
+const styles = StyleSheet.create({
13
+  card: {
14
+    margin: 10,
15
+  },
16
+  icon: {
17
+    backgroundColor: 'transparent',
18
+  },
19
+});
20
+
21
+export default function ProfileClubCard(props: Props) {
22
+  const theme = useTheme();
23
+  const navigation = useNavigation();
24
+
25
+  const clubKeyExtractor = (item: ProfileClubType) => item.name;
26
+
27
+  const getClubListItem = ({ item }: { item: ProfileClubType }) => {
28
+    const onPress = () =>
29
+      navigation.navigate('club-information', { clubId: item.id });
30
+    let description = i18n.t('screens.profile.isMember');
31
+    let icon = (leftProps: {
32
+      color: string;
33
+      style: {
34
+        marginLeft: number;
35
+        marginRight: number;
36
+        marginVertical?: number;
37
+      };
38
+    }) => (
39
+      <List.Icon
40
+        color={leftProps.color}
41
+        style={leftProps.style}
42
+        icon="chevron-right"
43
+      />
44
+    );
45
+    if (item.is_manager) {
46
+      description = i18n.t('screens.profile.isManager');
47
+      icon = (leftProps) => (
48
+        <List.Icon
49
+          style={leftProps.style}
50
+          icon="star"
51
+          color={theme.colors.primary}
52
+        />
53
+      );
54
+    }
55
+    return (
56
+      <List.Item
57
+        title={item.name}
58
+        description={description}
59
+        left={icon}
60
+        onPress={onPress}
61
+      />
62
+    );
63
+  };
64
+
65
+  function getClubList(list: Array<ProfileClubType> | undefined) {
66
+    if (!list) {
67
+      return null;
68
+    }
69
+
70
+    list.sort((a) => (a.is_manager ? -1 : 1));
71
+    return (
72
+      <FlatList
73
+        renderItem={getClubListItem}
74
+        keyExtractor={clubKeyExtractor}
75
+        data={list}
76
+      />
77
+    );
78
+  }
79
+
80
+  return (
81
+    <Card style={styles.card}>
82
+      <Card.Title
83
+        title={i18n.t('screens.profile.clubs')}
84
+        subtitle={i18n.t('screens.profile.clubsSubtitle')}
85
+        left={(iconProps) => (
86
+          <Avatar.Icon
87
+            size={iconProps.size}
88
+            icon="account-group"
89
+            color={theme.colors.primary}
90
+            style={styles.icon}
91
+          />
92
+        )}
93
+      />
94
+      <Card.Content>
95
+        <Divider />
96
+        {getClubList(props.clubs)}
97
+      </Card.Content>
98
+    </Card>
99
+  );
100
+}

+ 56
- 0
src/components/Amicale/Profile/ProfileMembershipCard.tsx View File

1
+import React from 'react';
2
+import { Avatar, Card, List, useTheme } from 'react-native-paper';
3
+import i18n from 'i18n-js';
4
+import { StyleSheet } from 'react-native';
5
+
6
+type Props = {
7
+  valid?: boolean;
8
+};
9
+
10
+const styles = StyleSheet.create({
11
+  card: {
12
+    margin: 10,
13
+  },
14
+  icon: {
15
+    backgroundColor: 'transparent',
16
+  },
17
+});
18
+
19
+export default function ProfileMembershipCard(props: Props) {
20
+  const theme = useTheme();
21
+  const state = props.valid === true;
22
+  return (
23
+    <Card style={styles.card}>
24
+      <Card.Title
25
+        title={i18n.t('screens.profile.membership')}
26
+        subtitle={i18n.t('screens.profile.membershipSubtitle')}
27
+        left={(iconProps) => (
28
+          <Avatar.Icon
29
+            size={iconProps.size}
30
+            icon="credit-card"
31
+            color={theme.colors.primary}
32
+            style={styles.icon}
33
+          />
34
+        )}
35
+      />
36
+      <Card.Content>
37
+        <List.Section>
38
+          <List.Item
39
+            title={
40
+              state
41
+                ? i18n.t('screens.profile.membershipPayed')
42
+                : i18n.t('screens.profile.membershipNotPayed')
43
+            }
44
+            left={(leftProps) => (
45
+              <List.Icon
46
+                style={leftProps.style}
47
+                color={state ? theme.colors.success : theme.colors.danger}
48
+                icon={state ? 'check' : 'close'}
49
+              />
50
+            )}
51
+          />
52
+        </List.Section>
53
+      </Card.Content>
54
+    </Card>
55
+  );
56
+}

+ 110
- 0
src/components/Amicale/Profile/ProfilePersonalCard.tsx View File

1
+import { useNavigation } from '@react-navigation/core';
2
+import React from 'react';
3
+import { StyleSheet } from 'react-native';
4
+import {
5
+  Avatar,
6
+  Button,
7
+  Card,
8
+  Divider,
9
+  List,
10
+  useTheme,
11
+} from 'react-native-paper';
12
+import Urls from '../../../constants/Urls';
13
+import { ProfileDataType } from '../../../screens/Amicale/ProfileScreen';
14
+import i18n from 'i18n-js';
15
+
16
+type Props = {
17
+  profile?: ProfileDataType;
18
+};
19
+
20
+const styles = StyleSheet.create({
21
+  card: {
22
+    margin: 10,
23
+  },
24
+  icon: {
25
+    backgroundColor: 'transparent',
26
+  },
27
+  editButton: {
28
+    marginLeft: 'auto',
29
+  },
30
+  mascot: {
31
+    width: 60,
32
+  },
33
+  title: {
34
+    marginLeft: 10,
35
+  },
36
+});
37
+
38
+function getFieldValue(field?: string): string {
39
+  return field ? field : i18n.t('screens.profile.noData');
40
+}
41
+
42
+export default function ProfilePersonalCard(props: Props) {
43
+  const { profile } = props;
44
+  const theme = useTheme();
45
+  const navigation = useNavigation();
46
+
47
+  function getPersonalListItem(field: string | undefined, icon: string) {
48
+    const title = field != null ? getFieldValue(field) : ':(';
49
+    const subtitle = field != null ? '' : getFieldValue(field);
50
+    return (
51
+      <List.Item
52
+        title={title}
53
+        description={subtitle}
54
+        left={(leftProps) => (
55
+          <List.Icon
56
+            style={leftProps.style}
57
+            icon={icon}
58
+            color={field != null ? leftProps.color : theme.colors.textDisabled}
59
+          />
60
+        )}
61
+      />
62
+    );
63
+  }
64
+
65
+  return (
66
+    <Card style={styles.card}>
67
+      <Card.Title
68
+        title={`${profile?.first_name} ${profile?.last_name}`}
69
+        subtitle={profile?.email}
70
+        left={(iconProps) => (
71
+          <Avatar.Icon
72
+            size={iconProps.size}
73
+            icon="account"
74
+            color={theme.colors.primary}
75
+            style={styles.icon}
76
+          />
77
+        )}
78
+      />
79
+      <Card.Content>
80
+        <Divider />
81
+        <List.Section>
82
+          <List.Subheader>
83
+            {i18n.t('screens.profile.personalInformation')}
84
+          </List.Subheader>
85
+          {getPersonalListItem(profile?.birthday, 'cake-variant')}
86
+          {getPersonalListItem(profile?.phone, 'phone')}
87
+          {getPersonalListItem(profile?.email, 'email')}
88
+          {getPersonalListItem(profile?.branch, 'school')}
89
+        </List.Section>
90
+        <Divider />
91
+        <Card.Actions>
92
+          <Button
93
+            icon="account-edit"
94
+            mode="contained"
95
+            onPress={() => {
96
+              navigation.navigate('website', {
97
+                host: Urls.websites.amicale,
98
+                path: profile?.link,
99
+                title: i18n.t('screens.websites.amicale'),
100
+              });
101
+            }}
102
+            style={styles.editButton}
103
+          >
104
+            {i18n.t('screens.profile.editInformation')}
105
+          </Button>
106
+        </Card.Actions>
107
+      </Card.Content>
108
+    </Card>
109
+  );
110
+}

+ 81
- 0
src/components/Amicale/Profile/ProfileWelcomeCard.tsx View File

1
+import { useNavigation } from '@react-navigation/core';
2
+import React from 'react';
3
+import { Button, Card, Divider, Paragraph } from 'react-native-paper';
4
+import Mascot, { MASCOT_STYLE } from '../../Mascot/Mascot';
5
+import i18n from 'i18n-js';
6
+import { StyleSheet } from 'react-native';
7
+import CardList from '../../Lists/CardList/CardList';
8
+import { getAmicaleServices, SERVICES_KEY } from '../../../utils/Services';
9
+
10
+type Props = {
11
+  firstname?: string;
12
+};
13
+
14
+const styles = StyleSheet.create({
15
+  card: {
16
+    margin: 10,
17
+  },
18
+  editButton: {
19
+    marginLeft: 'auto',
20
+  },
21
+  mascot: {
22
+    width: 60,
23
+  },
24
+  title: {
25
+    marginLeft: 10,
26
+  },
27
+});
28
+
29
+function ProfileWelcomeCard(props: Props) {
30
+  const navigation = useNavigation();
31
+  return (
32
+    <Card style={styles.card}>
33
+      <Card.Title
34
+        title={i18n.t('screens.profile.welcomeTitle', {
35
+          name: props.firstname,
36
+        })}
37
+        left={() => (
38
+          <Mascot
39
+            style={styles.mascot}
40
+            emotion={MASCOT_STYLE.COOL}
41
+            animated
42
+            entryAnimation={{
43
+              animation: 'bounceIn',
44
+              duration: 1000,
45
+            }}
46
+          />
47
+        )}
48
+        titleStyle={styles.title}
49
+      />
50
+      <Card.Content>
51
+        <Divider />
52
+        <Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
53
+        <CardList
54
+          dataset={getAmicaleServices(navigation.navigate, [
55
+            SERVICES_KEY.PROFILE,
56
+          ])}
57
+          isHorizontal={true}
58
+        />
59
+        <Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
60
+        <Divider />
61
+        <Card.Actions>
62
+          <Button
63
+            icon="bug"
64
+            mode="contained"
65
+            onPress={() => {
66
+              navigation.navigate('feedback');
67
+            }}
68
+            style={styles.editButton}
69
+          >
70
+            {i18n.t('screens.feedback.homeButtonTitle')}
71
+          </Button>
72
+        </Card.Actions>
73
+      </Card.Content>
74
+    </Card>
75
+  );
76
+}
77
+
78
+export default React.memo(
79
+  ProfileWelcomeCard,
80
+  (pp, np) => pp.firstname === np.firstname
81
+);

+ 75
- 102
src/components/Amicale/Vote/VoteSelect.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
20
+import React, { useState } from 'react';
21
 import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
21
 import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
22
 import { FlatList, StyleSheet, View } from 'react-native';
22
 import { FlatList, StyleSheet, View } from 'react-native';
23
 import i18n from 'i18n-js';
23
 import i18n from 'i18n-js';
24
-import ConnectionManager from '../../../managers/ConnectionManager';
25
 import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
24
 import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
26
 import ErrorDialog from '../../Dialogs/ErrorDialog';
25
 import ErrorDialog from '../../Dialogs/ErrorDialog';
27
 import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
26
 import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
28
 import { ApiRejectType } from '../../../utils/WebData';
27
 import { ApiRejectType } from '../../../utils/WebData';
29
 import { REQUEST_STATUS } from '../../../utils/Requests';
28
 import { REQUEST_STATUS } from '../../../utils/Requests';
29
+import { useAuthenticatedRequest } from '../../../context/loginContext';
30
 
30
 
31
-type PropsType = {
31
+type Props = {
32
   teams: Array<VoteTeamType>;
32
   teams: Array<VoteTeamType>;
33
   onVoteSuccess: () => void;
33
   onVoteSuccess: () => void;
34
   onVoteError: () => void;
34
   onVoteError: () => void;
35
 };
35
 };
36
 
36
 
37
-type StateType = {
38
-  selectedTeam: string;
39
-  voteDialogVisible: boolean;
40
-  errorDialogVisible: boolean;
41
-  currentError: ApiRejectType;
42
-};
43
-
44
 const styles = StyleSheet.create({
37
 const styles = StyleSheet.create({
45
   card: {
38
   card: {
46
     margin: 10,
39
     margin: 10,
50
   },
43
   },
51
 });
44
 });
52
 
45
 
53
-export default class VoteSelect extends React.PureComponent<
54
-  PropsType,
55
-  StateType
56
-> {
57
-  constructor(props: PropsType) {
58
-    super(props);
59
-    this.state = {
60
-      selectedTeam: 'none',
61
-      voteDialogVisible: false,
62
-      errorDialogVisible: false,
63
-      currentError: { status: REQUEST_STATUS.SUCCESS },
64
-    };
65
-  }
66
-
67
-  onVoteSelectionChange = (teamName: string): void =>
68
-    this.setState({ selectedTeam: teamName });
46
+function VoteSelect(props: Props) {
47
+  const [selectedTeam, setSelectedTeam] = useState('none');
48
+  const [voteDialogVisible, setVoteDialogVisible] = useState(false);
49
+  const [currentError, setCurrentError] = useState<ApiRejectType>({
50
+    status: REQUEST_STATUS.SUCCESS,
51
+  });
52
+  const request = useAuthenticatedRequest('elections/vote', {
53
+    team: parseInt(selectedTeam, 10),
54
+  });
69
 
55
 
70
-  voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
56
+  const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
71
 
57
 
72
-  voteRenderItem = ({ item }: { item: VoteTeamType }) => (
58
+  const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
73
     <RadioButton.Item label={item.name} value={item.id.toString()} />
59
     <RadioButton.Item label={item.name} value={item.id.toString()} />
74
   );
60
   );
75
 
61
 
76
-  showVoteDialog = (): void => this.setState({ voteDialogVisible: true });
62
+  const showVoteDialog = () => setVoteDialogVisible(true);
77
 
63
 
78
-  onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false });
64
+  const onVoteDialogDismiss = () => setVoteDialogVisible(false);
79
 
65
 
80
-  onVoteDialogAccept = async (): Promise<void> => {
66
+  const onVoteDialogAccept = async (): Promise<void> => {
81
     return new Promise((resolve: () => void) => {
67
     return new Promise((resolve: () => void) => {
82
-      const { state } = this;
83
-      ConnectionManager.getInstance()
84
-        .authenticatedRequest('elections/vote', {
85
-          team: parseInt(state.selectedTeam, 10),
86
-        })
68
+      request()
87
         .then(() => {
69
         .then(() => {
88
-          this.onVoteDialogDismiss();
89
-          const { props } = this;
70
+          onVoteDialogDismiss();
90
           props.onVoteSuccess();
71
           props.onVoteSuccess();
91
           resolve();
72
           resolve();
92
         })
73
         })
93
         .catch((error: ApiRejectType) => {
74
         .catch((error: ApiRejectType) => {
94
-          this.onVoteDialogDismiss();
95
-          this.showErrorDialog(error);
75
+          onVoteDialogDismiss();
76
+          setCurrentError(error);
96
           resolve();
77
           resolve();
97
         });
78
         });
98
     });
79
     });
99
   };
80
   };
100
 
81
 
101
-  showErrorDialog = (error: ApiRejectType): void =>
102
-    this.setState({
103
-      errorDialogVisible: true,
104
-      currentError: error,
105
-    });
106
-
107
-  onErrorDialogDismiss = () => {
108
-    this.setState({ errorDialogVisible: false });
109
-    const { props } = this;
82
+  const onErrorDialogDismiss = () => {
83
+    setCurrentError({ status: REQUEST_STATUS.SUCCESS });
110
     props.onVoteError();
84
     props.onVoteError();
111
   };
85
   };
112
 
86
 
113
-  render() {
114
-    const { state, props } = this;
115
-    return (
116
-      <View>
117
-        <Card style={styles.card}>
118
-          <Card.Title
119
-            title={i18n.t('screens.vote.select.title')}
120
-            subtitle={i18n.t('screens.vote.select.subtitle')}
121
-            left={(iconProps) => (
122
-              <Avatar.Icon size={iconProps.size} icon="alert-decagram" />
123
-            )}
124
-          />
125
-          <Card.Content>
126
-            <RadioButton.Group
127
-              onValueChange={this.onVoteSelectionChange}
128
-              value={state.selectedTeam}
129
-            >
130
-              <FlatList
131
-                data={props.teams}
132
-                keyExtractor={this.voteKeyExtractor}
133
-                extraData={state.selectedTeam}
134
-                renderItem={this.voteRenderItem}
135
-              />
136
-            </RadioButton.Group>
137
-          </Card.Content>
138
-          <Card.Actions>
139
-            <Button
140
-              icon="send"
141
-              mode="contained"
142
-              onPress={this.showVoteDialog}
143
-              style={styles.button}
144
-              disabled={state.selectedTeam === 'none'}
145
-            >
146
-              {i18n.t('screens.vote.select.sendButton')}
147
-            </Button>
148
-          </Card.Actions>
149
-        </Card>
150
-        <LoadingConfirmDialog
151
-          visible={state.voteDialogVisible}
152
-          onDismiss={this.onVoteDialogDismiss}
153
-          onAccept={this.onVoteDialogAccept}
154
-          title={i18n.t('screens.vote.select.dialogTitle')}
155
-          titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
156
-          message={i18n.t('screens.vote.select.dialogMessage')}
157
-        />
158
-        <ErrorDialog
159
-          visible={state.errorDialogVisible}
160
-          onDismiss={this.onErrorDialogDismiss}
161
-          status={state.currentError.status}
162
-          code={state.currentError.code}
87
+  return (
88
+    <View>
89
+      <Card style={styles.card}>
90
+        <Card.Title
91
+          title={i18n.t('screens.vote.select.title')}
92
+          subtitle={i18n.t('screens.vote.select.subtitle')}
93
+          left={(iconProps) => (
94
+            <Avatar.Icon size={iconProps.size} icon="alert-decagram" />
95
+          )}
163
         />
96
         />
164
-      </View>
165
-    );
166
-  }
97
+        <Card.Content>
98
+          <RadioButton.Group
99
+            onValueChange={setSelectedTeam}
100
+            value={selectedTeam}
101
+          >
102
+            <FlatList
103
+              data={props.teams}
104
+              keyExtractor={voteKeyExtractor}
105
+              extraData={selectedTeam}
106
+              renderItem={voteRenderItem}
107
+            />
108
+          </RadioButton.Group>
109
+        </Card.Content>
110
+        <Card.Actions>
111
+          <Button
112
+            icon={'send'}
113
+            mode={'contained'}
114
+            onPress={showVoteDialog}
115
+            style={styles.button}
116
+            disabled={selectedTeam === 'none'}
117
+          >
118
+            {i18n.t('screens.vote.select.sendButton')}
119
+          </Button>
120
+        </Card.Actions>
121
+      </Card>
122
+      <LoadingConfirmDialog
123
+        visible={voteDialogVisible}
124
+        onDismiss={onVoteDialogDismiss}
125
+        onAccept={onVoteDialogAccept}
126
+        title={i18n.t('screens.vote.select.dialogTitle')}
127
+        titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
128
+        message={i18n.t('screens.vote.select.dialogMessage')}
129
+      />
130
+      <ErrorDialog
131
+        visible={currentError.status !== REQUEST_STATUS.SUCCESS}
132
+        onDismiss={onErrorDialogDismiss}
133
+        status={currentError.status}
134
+        code={currentError.code}
135
+      />
136
+    </View>
137
+  );
167
 }
138
 }
139
+
140
+export default VoteSelect;

+ 3
- 3
src/components/Lists/Equipment/EquipmentListItem.tsx View File

20
 import * as React from 'react';
20
 import * as React from 'react';
21
 import { Avatar, List, useTheme } from 'react-native-paper';
21
 import { Avatar, List, useTheme } from 'react-native-paper';
22
 import i18n from 'i18n-js';
22
 import i18n from 'i18n-js';
23
-import { StackNavigationProp } from '@react-navigation/stack';
24
 import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
23
 import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
25
 import {
24
 import {
26
   getFirstEquipmentAvailability,
25
   getFirstEquipmentAvailability,
29
 } from '../../../utils/EquipmentBooking';
28
 } from '../../../utils/EquipmentBooking';
30
 import { StyleSheet } from 'react-native';
29
 import { StyleSheet } from 'react-native';
31
 import GENERAL_STYLES from '../../../constants/Styles';
30
 import GENERAL_STYLES from '../../../constants/Styles';
31
+import { useNavigation } from '@react-navigation/native';
32
 
32
 
33
 type PropsType = {
33
 type PropsType = {
34
-  navigation: StackNavigationProp<any>;
35
   userDeviceRentDates: [string, string] | null;
34
   userDeviceRentDates: [string, string] | null;
36
   item: DeviceType;
35
   item: DeviceType;
37
   height: number;
36
   height: number;
48
 
47
 
49
 function EquipmentListItem(props: PropsType) {
48
 function EquipmentListItem(props: PropsType) {
50
   const theme = useTheme();
49
   const theme = useTheme();
51
-  const { item, userDeviceRentDates, navigation, height } = props;
50
+  const navigation = useNavigation();
51
+  const { item, userDeviceRentDates, height } = props;
52
   const isRented = userDeviceRentDates != null;
52
   const isRented = userDeviceRentDates != null;
53
   const isAvailable = isEquipmentAvailable(item);
53
   const isAvailable = isEquipmentAvailable(item);
54
   const firstAvailability = getFirstEquipmentAvailability(item);
54
   const firstAvailability = getFirstEquipmentAvailability(item);

+ 5
- 7
src/components/Screens/RequestScreen.tsx View File

11
 import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
11
 import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
12
 import { StackNavigationProp } from '@react-navigation/stack';
12
 import { StackNavigationProp } from '@react-navigation/stack';
13
 import { MainRoutes } from '../../navigation/MainNavigator';
13
 import { MainRoutes } from '../../navigation/MainNavigator';
14
-import ConnectionManager from '../../managers/ConnectionManager';
14
+import { useLogout } from '../../utils/logout';
15
 
15
 
16
 export type RequestScreenProps<T> = {
16
 export type RequestScreenProps<T> = {
17
   request: () => Promise<T>;
17
   request: () => Promise<T>;
44
 const MIN_REFRESH_TIME = 3 * 1000;
44
 const MIN_REFRESH_TIME = 3 * 1000;
45
 
45
 
46
 export default function RequestScreen<T>(props: Props<T>) {
46
 export default function RequestScreen<T>(props: Props<T>) {
47
+  const onLogout = useLogout();
47
   const navigation = useNavigation<StackNavigationProp<any>>();
48
   const navigation = useNavigation<StackNavigationProp<any>>();
48
   const route = useRoute();
49
   const route = useRoute();
49
   const refreshInterval = useRef<number>();
50
   const refreshInterval = useRef<number>();
103
 
104
 
104
   useEffect(() => {
105
   useEffect(() => {
105
     if (isErrorCritical(code)) {
106
     if (isErrorCritical(code)) {
106
-      ConnectionManager.getInstance()
107
-        .disconnect()
108
-        .then(() => {
109
-          navigation.replace(MainRoutes.Login, { nextScreen: route.name });
110
-        });
107
+      onLogout();
108
+      navigation.replace(MainRoutes.Login, { nextScreen: route.name });
111
     }
109
     }
112
-  }, [code, navigation, route]);
110
+  }, [code, navigation, route, onLogout]);
113
 
111
 
114
   if (data === undefined && loading && props.showLoading !== false) {
112
   if (data === undefined && loading && props.showLoading !== false) {
115
     return <BasicLoadingScreen />;
113
     return <BasicLoadingScreen />;

+ 27
- 0
src/components/providers/LoginProvider.tsx View File

1
+import React, { useState } from 'react';
2
+import { LoginContext, LoginContextType } from '../../context/loginContext';
3
+
4
+type Props = {
5
+  children: React.ReactChild;
6
+  initialToken: string | undefined;
7
+};
8
+
9
+export default function LoginProvider(props: Props) {
10
+  const setLogin = (token: string | undefined) => {
11
+    setLoginState((prevState) => ({
12
+      ...prevState,
13
+      token,
14
+    }));
15
+  };
16
+
17
+  const [loginState, setLoginState] = useState<LoginContextType>({
18
+    token: props.initialToken,
19
+    setLogin: setLogin,
20
+  });
21
+
22
+  return (
23
+    <LoginContext.Provider value={loginState}>
24
+      {props.children}
25
+    </LoginContext.Provider>
26
+  );
27
+}

+ 46
- 0
src/context/loginContext.ts View File

1
+import React, { useContext } from 'react';
2
+import { apiRequest } from '../utils/WebData';
3
+
4
+export type LoginContextType = {
5
+  token: string | undefined;
6
+  setLogin: (token: string | undefined) => void;
7
+};
8
+
9
+export const LoginContext = React.createContext<LoginContextType>({
10
+  token: undefined,
11
+  setLogin: () => undefined,
12
+});
13
+
14
+/**
15
+ * Hook used to retrieve the user token and puid.
16
+ * @returns Login context with token and puid to undefined if user is not logged in
17
+ */
18
+export function useLogin() {
19
+  return useContext(LoginContext);
20
+}
21
+
22
+/**
23
+ * Checks if the user is connected
24
+ * @returns True if the user is connected
25
+ */
26
+export function useLoginState() {
27
+  const { token } = useLogin();
28
+  return token !== undefined;
29
+}
30
+
31
+/**
32
+ * Gets the current user token.
33
+ * @returns The token, or empty string if the user is not logged in
34
+ */
35
+export function useLoginToken() {
36
+  const { token } = useLogin();
37
+  return token ? token : '';
38
+}
39
+
40
+export function useAuthenticatedRequest<T>(
41
+  path: string,
42
+  params?: { [key: string]: any }
43
+) {
44
+  const token = useLoginToken();
45
+  return () => apiRequest<T>(path, 'POST', params, token);
46
+}

+ 0
- 205
src/managers/ConnectionManager.ts View File

1
-/*
2
- * Copyright (c) 2019 - 2020 Arnaud Vergnet.
3
- *
4
- * This file is part of Campus INSAT.
5
- *
6
- * Campus INSAT is free software: you can redistribute it and/or modify
7
- *  it under the terms of the GNU General Public License as published by
8
- * the Free Software Foundation, either version 3 of the License, or
9
- * (at your option) any later version.
10
- *
11
- * Campus INSAT is distributed in the hope that it will be useful,
12
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
- * GNU General Public License for more details.
15
- *
16
- * You should have received a copy of the GNU General Public License
17
- * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
- */
19
-
20
-import * as Keychain from 'react-native-keychain';
21
-import { REQUEST_STATUS } from '../utils/Requests';
22
-import type { ApiDataLoginType, ApiRejectType } from '../utils/WebData';
23
-import { apiRequest } from '../utils/WebData';
24
-
25
-/**
26
- * champ: error
27
- *
28
- * 0 : SUCCESS -> pas d'erreurs
29
- * 1 : BAD_CREDENTIALS -> email ou mdp invalide
30
- * 2 : BAD_TOKEN -> session expirée
31
- * 3 : NO_CONSENT
32
- * 403 : FORBIDDEN -> accès a la ressource interdit
33
- * 500 : SERVER_ERROR -> pb coté serveur
34
- */
35
-
36
-const AUTH_PATH = 'password';
37
-
38
-export default class ConnectionManager {
39
-  static instance: ConnectionManager | null = null;
40
-
41
-  private token: string | null;
42
-
43
-  constructor() {
44
-    this.token = null;
45
-  }
46
-
47
-  /**
48
-   * Gets this class instance or create one if none is found
49
-   *
50
-   * @returns {ConnectionManager}
51
-   */
52
-  static getInstance(): ConnectionManager {
53
-    if (ConnectionManager.instance == null) {
54
-      ConnectionManager.instance = new ConnectionManager();
55
-    }
56
-    return ConnectionManager.instance;
57
-  }
58
-
59
-  /**
60
-   * Gets the current token
61
-   *
62
-   * @returns {string | null}
63
-   */
64
-  getToken(): string | null {
65
-    return this.token;
66
-  }
67
-
68
-  /**
69
-   * Tries to recover login token from the secure keychain
70
-   *
71
-   * @returns Promise<void>
72
-   */
73
-  async recoverLogin(): Promise<void> {
74
-    return new Promise((resolve: () => void) => {
75
-      const token = this.getToken();
76
-      if (token != null) {
77
-        resolve();
78
-      } else {
79
-        Keychain.getGenericPassword()
80
-          .then((data: Keychain.UserCredentials | false) => {
81
-            if (data && data.password != null) {
82
-              this.token = data.password;
83
-            }
84
-            resolve();
85
-          })
86
-          .catch(() => resolve());
87
-      }
88
-    });
89
-  }
90
-
91
-  /**
92
-   * Check if the user has a valid token
93
-   *
94
-   * @returns {boolean}
95
-   */
96
-  isLoggedIn(): boolean {
97
-    return this.getToken() !== null;
98
-  }
99
-
100
-  /**
101
-   * Saves the login token in the secure keychain
102
-   *
103
-   * @param email
104
-   * @param token
105
-   * @returns Promise<void>
106
-   */
107
-  async saveLogin(_email: string, token: string): Promise<void> {
108
-    return new Promise((resolve: () => void, reject: () => void) => {
109
-      Keychain.setGenericPassword('token', token)
110
-        .then(() => {
111
-          this.token = token;
112
-          resolve();
113
-        })
114
-        .catch((): void => reject());
115
-    });
116
-  }
117
-
118
-  /**
119
-   * Deletes the login token from the keychain
120
-   *
121
-   * @returns Promise<void>
122
-   */
123
-  async disconnect(): Promise<void> {
124
-    return new Promise((resolve: () => void, reject: () => void) => {
125
-      Keychain.resetGenericPassword()
126
-        .then(() => {
127
-          this.token = null;
128
-          resolve();
129
-        })
130
-        .catch((): void => reject());
131
-    });
132
-  }
133
-
134
-  /**
135
-   * Sends the given login and password to the api.
136
-   * If the combination is valid, the login token is received and saved in the secure keychain.
137
-   * If not, the promise is rejected with the corresponding error code.
138
-   *
139
-   * @param email
140
-   * @param password
141
-   * @returns Promise<void>
142
-   */
143
-  async connect(email: string, password: string): Promise<void> {
144
-    return new Promise(
145
-      (resolve: () => void, reject: (error: ApiRejectType) => void) => {
146
-        const data = {
147
-          email,
148
-          password,
149
-        };
150
-        apiRequest<ApiDataLoginType>(AUTH_PATH, 'POST', data)
151
-          .then((response: ApiDataLoginType) => {
152
-            if (response.token != null) {
153
-              this.saveLogin(email, response.token)
154
-                .then(() => resolve())
155
-                .catch(() =>
156
-                  reject({
157
-                    status: REQUEST_STATUS.TOKEN_SAVE,
158
-                  })
159
-                );
160
-            } else {
161
-              reject({
162
-                status: REQUEST_STATUS.SERVER_ERROR,
163
-              });
164
-            }
165
-          })
166
-          .catch((err) => {
167
-            reject(err);
168
-          });
169
-      }
170
-    );
171
-  }
172
-
173
-  /**
174
-   * Sends an authenticated request with the login token to the API
175
-   *
176
-   * @param path
177
-   * @param params
178
-   * @returns Promise<ApiGenericDataType>
179
-   */
180
-  async authenticatedRequest<T>(
181
-    path: string,
182
-    params?: { [key: string]: any }
183
-  ): Promise<T> {
184
-    return new Promise(
185
-      (
186
-        resolve: (response: T) => void,
187
-        reject: (error: ApiRejectType) => void
188
-      ) => {
189
-        if (this.getToken() !== null) {
190
-          const data = {
191
-            ...params,
192
-            token: this.getToken(),
193
-          };
194
-          apiRequest<T>(path, 'POST', data)
195
-            .then((response: T) => resolve(response))
196
-            .catch(reject);
197
-        } else {
198
-          reject({
199
-            status: REQUEST_STATUS.TOKEN_RETRIEVE,
200
-          });
201
-        }
202
-      }
203
-    );
204
-  }
205
-}

+ 62
- 77
src/screens/Amicale/Clubs/ClubDisplayScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
20
+import React, { useState } from 'react';
21
 import { Linking, StyleSheet, View } from 'react-native';
21
 import { Linking, StyleSheet, View } from 'react-native';
22
 import {
22
 import {
23
   Avatar,
23
   Avatar,
25
   Card,
25
   Card,
26
   Chip,
26
   Chip,
27
   Paragraph,
27
   Paragraph,
28
-  withTheme,
28
+  useTheme,
29
 } from 'react-native-paper';
29
 } from 'react-native-paper';
30
 import i18n from 'i18n-js';
30
 import i18n from 'i18n-js';
31
-import { StackNavigationProp } from '@react-navigation/stack';
32
 import CustomHTML from '../../../components/Overrides/CustomHTML';
31
 import CustomHTML from '../../../components/Overrides/CustomHTML';
33
 import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
32
 import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
34
 import type { ClubCategoryType, ClubType } from './ClubListScreen';
33
 import type { ClubCategoryType, ClubType } from './ClubListScreen';
35
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
34
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
36
 import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
35
 import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
37
 import RequestScreen from '../../../components/Screens/RequestScreen';
36
 import RequestScreen from '../../../components/Screens/RequestScreen';
38
-import ConnectionManager from '../../../managers/ConnectionManager';
37
+import { useFocusEffect } from '@react-navigation/core';
38
+import { useCallback } from 'react';
39
+import { useNavigation } from '@react-navigation/native';
40
+import { useAuthenticatedRequest } from '../../../context/loginContext';
39
 
41
 
40
-type PropsType = {
41
-  navigation: StackNavigationProp<any>;
42
+type Props = {
42
   route: {
43
   route: {
43
     params?: {
44
     params?: {
44
       data?: ClubType;
45
       data?: ClubType;
46
       clubId?: number;
47
       clubId?: number;
47
     };
48
     };
48
   };
49
   };
49
-  theme: ReactNativePaper.Theme;
50
 };
50
 };
51
 
51
 
52
 type ResponseType = ClubType;
52
 type ResponseType = ClubType;
89
  * If called with data and categories navigation parameters, will use those to display the data.
89
  * If called with data and categories navigation parameters, will use those to display the data.
90
  * If called with clubId parameter, will fetch the information on the server
90
  * If called with clubId parameter, will fetch the information on the server
91
  */
91
  */
92
-class ClubDisplayScreen extends React.Component<PropsType> {
93
-  displayData: ClubType | undefined;
92
+function ClubDisplayScreen(props: Props) {
93
+  const navigation = useNavigation();
94
+  const theme = useTheme();
94
 
95
 
95
-  categories: Array<ClubCategoryType> | null;
96
+  const [displayData, setDisplayData] = useState<ClubType | undefined>();
97
+  const [categories, setCategories] = useState<
98
+    Array<ClubCategoryType> | undefined
99
+  >();
100
+  const [clubId, setClubId] = useState<number | undefined>();
96
 
101
 
97
-  clubId: number;
98
-
99
-  shouldFetchData: boolean;
100
-
101
-  constructor(props: PropsType) {
102
-    super(props);
103
-    this.displayData = undefined;
104
-    this.categories = null;
105
-    this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
106
-    this.shouldFetchData = true;
107
-
108
-    if (
109
-      props.route.params &&
110
-      props.route.params.data &&
111
-      props.route.params.categories
112
-    ) {
113
-      this.displayData = props.route.params.data;
114
-      this.categories = props.route.params.categories;
115
-      this.clubId = props.route.params.data.id;
116
-      this.shouldFetchData = false;
117
-    }
118
-  }
102
+  useFocusEffect(
103
+    useCallback(() => {
104
+      if (props.route.params?.data && props.route.params?.categories) {
105
+        setDisplayData(props.route.params.data);
106
+        setCategories(props.route.params.categories);
107
+        setClubId(props.route.params.data.id);
108
+      } else {
109
+        const id = props.route.params?.clubId;
110
+        setClubId(id ? id : 0);
111
+      }
112
+    }, [props.route.params])
113
+  );
119
 
114
 
120
   /**
115
   /**
121
    * Gets the name of the category with the given ID
116
    * Gets the name of the category with the given ID
123
    * @param id The category's ID
118
    * @param id The category's ID
124
    * @returns {string|*}
119
    * @returns {string|*}
125
    */
120
    */
126
-  getCategoryName(id: number): string {
121
+  const getCategoryName = (id: number): string => {
127
     let categoryName = '';
122
     let categoryName = '';
128
-    if (this.categories !== null) {
129
-      this.categories.forEach((item: ClubCategoryType) => {
123
+    if (categories) {
124
+      categories.forEach((item: ClubCategoryType) => {
130
         if (id === item.id) {
125
         if (id === item.id) {
131
           categoryName = item.name;
126
           categoryName = item.name;
132
         }
127
         }
133
       });
128
       });
134
     }
129
     }
135
     return categoryName;
130
     return categoryName;
136
-  }
131
+  };
137
 
132
 
138
   /**
133
   /**
139
    * Gets the view for rendering categories
134
    * Gets the view for rendering categories
141
    * @param categories The categories to display (max 2)
136
    * @param categories The categories to display (max 2)
142
    * @returns {null|*}
137
    * @returns {null|*}
143
    */
138
    */
144
-  getCategoriesRender(categories: Array<number | null>) {
145
-    if (this.categories == null) {
139
+  const getCategoriesRender = (c: Array<number | null>) => {
140
+    if (!categories) {
146
       return null;
141
       return null;
147
     }
142
     }
148
 
143
 
149
     const final: Array<React.ReactNode> = [];
144
     const final: Array<React.ReactNode> = [];
150
-    categories.forEach((cat: number | null) => {
145
+    c.forEach((cat: number | null) => {
151
       if (cat != null) {
146
       if (cat != null) {
152
         final.push(
147
         final.push(
153
           <Chip style={styles.category} key={cat}>
148
           <Chip style={styles.category} key={cat}>
154
-            {this.getCategoryName(cat)}
149
+            {getCategoryName(cat)}
155
           </Chip>
150
           </Chip>
156
         );
151
         );
157
       }
152
       }
158
     });
153
     });
159
     return <View style={styles.categoryContainer}>{final}</View>;
154
     return <View style={styles.categoryContainer}>{final}</View>;
160
-  }
155
+  };
161
 
156
 
162
   /**
157
   /**
163
    * Gets the view for rendering club managers if any
158
    * Gets the view for rendering club managers if any
166
    * @param email The club contact email
161
    * @param email The club contact email
167
    * @returns {*}
162
    * @returns {*}
168
    */
163
    */
169
-  getManagersRender(managers: Array<string>, email: string | null) {
170
-    const { props } = this;
164
+  const getManagersRender = (managers: Array<string>, email: string | null) => {
171
     const managersListView: Array<React.ReactNode> = [];
165
     const managersListView: Array<React.ReactNode> = [];
172
     managers.forEach((item: string) => {
166
     managers.forEach((item: string) => {
173
       managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
167
       managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
191
             <Avatar.Icon
185
             <Avatar.Icon
192
               size={iconProps.size}
186
               size={iconProps.size}
193
               style={styles.icon}
187
               style={styles.icon}
194
-              color={
195
-                hasManagers
196
-                  ? props.theme.colors.success
197
-                  : props.theme.colors.primary
198
-              }
188
+              color={hasManagers ? theme.colors.success : theme.colors.primary}
199
               icon="account-tie"
189
               icon="account-tie"
200
             />
190
             />
201
           )}
191
           )}
202
         />
192
         />
203
         <Card.Content>
193
         <Card.Content>
204
           {managersListView}
194
           {managersListView}
205
-          {ClubDisplayScreen.getEmailButton(email, hasManagers)}
195
+          {getEmailButton(email, hasManagers)}
206
         </Card.Content>
196
         </Card.Content>
207
       </Card>
197
       </Card>
208
     );
198
     );
209
-  }
199
+  };
210
 
200
 
211
   /**
201
   /**
212
    * Gets the email button to contact the club, or the amicale if the club does not have any managers
202
    * Gets the email button to contact the club, or the amicale if the club does not have any managers
215
    * @param hasManagers True if the club has managers
205
    * @param hasManagers True if the club has managers
216
    * @returns {*}
206
    * @returns {*}
217
    */
207
    */
218
-  static getEmailButton(email: string | null, hasManagers: boolean) {
208
+  const getEmailButton = (email: string | null, hasManagers: boolean) => {
219
     const destinationEmail =
209
     const destinationEmail =
220
       email != null && hasManagers ? email : AMICALE_MAIL;
210
       email != null && hasManagers ? email : AMICALE_MAIL;
221
     const text =
211
     const text =
236
         </Button>
226
         </Button>
237
       </Card.Actions>
227
       </Card.Actions>
238
     );
228
     );
239
-  }
229
+  };
240
 
230
 
241
-  getScreen = (data: ResponseType | undefined) => {
231
+  const getScreen = (data: ResponseType | undefined) => {
242
     if (data) {
232
     if (data) {
243
-      this.updateHeaderTitle(data);
233
+      updateHeaderTitle(data);
244
       return (
234
       return (
245
         <CollapsibleScrollView style={styles.scroll} hasTab>
235
         <CollapsibleScrollView style={styles.scroll} hasTab>
246
-          {this.getCategoriesRender(data.category)}
236
+          {getCategoriesRender(data.category)}
247
           {data.logo !== null ? (
237
           {data.logo !== null ? (
248
             <ImageGalleryButton
238
             <ImageGalleryButton
249
               images={[{ url: data.logo }]}
239
               images={[{ url: data.logo }]}
261
           ) : (
251
           ) : (
262
             <View />
252
             <View />
263
           )}
253
           )}
264
-          {this.getManagersRender(data.responsibles, data.email)}
254
+          {getManagersRender(data.responsibles, data.email)}
265
         </CollapsibleScrollView>
255
         </CollapsibleScrollView>
266
       );
256
       );
267
     }
257
     }
273
    *
263
    *
274
    * @param data The club data
264
    * @param data The club data
275
    */
265
    */
276
-  updateHeaderTitle(data: ClubType) {
277
-    const { props } = this;
278
-    props.navigation.setOptions({ title: data.name });
279
-  }
266
+  const updateHeaderTitle = (data: ClubType) => {
267
+    navigation.setOptions({ title: data.name });
268
+  };
280
 
269
 
281
-  render() {
282
-    if (this.shouldFetchData) {
283
-      return (
284
-        <RequestScreen
285
-          request={() =>
286
-            ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
287
-              'clubs/info',
288
-              { id: this.clubId }
289
-            )
290
-          }
291
-          render={this.getScreen}
292
-        />
293
-      );
294
-    }
295
-    return this.getScreen(this.displayData);
296
-  }
270
+  const request = useAuthenticatedRequest<ClubType>('clubs/info', {
271
+    id: clubId,
272
+  });
273
+
274
+  return (
275
+    <RequestScreen
276
+      request={request}
277
+      render={getScreen}
278
+      cache={displayData}
279
+      onCacheUpdate={setDisplayData}
280
+    />
281
+  );
297
 }
282
 }
298
 
283
 
299
-export default withTheme(ClubDisplayScreen);
284
+export default ClubDisplayScreen;

+ 87
- 132
src/screens/Amicale/Clubs/ClubListScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
20
+import React, { useLayoutEffect, useRef, useState } from 'react';
21
 import { Platform } from 'react-native';
21
 import { Platform } from 'react-native';
22
 import { Searchbar } from 'react-native-paper';
22
 import { Searchbar } from 'react-native-paper';
23
 import i18n from 'i18n-js';
23
 import i18n from 'i18n-js';
24
-import { StackNavigationProp } from '@react-navigation/stack';
25
 import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
24
 import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
26
 import {
25
 import {
27
   isItemInCategoryFilter,
26
   isItemInCategoryFilter,
31
 import MaterialHeaderButtons, {
30
 import MaterialHeaderButtons, {
32
   Item,
31
   Item,
33
 } from '../../../components/Overrides/CustomHeaderButton';
32
 } from '../../../components/Overrides/CustomHeaderButton';
34
-import ConnectionManager from '../../../managers/ConnectionManager';
35
 import WebSectionList from '../../../components/Screens/WebSectionList';
33
 import WebSectionList from '../../../components/Screens/WebSectionList';
34
+import { useNavigation } from '@react-navigation/native';
35
+import { useAuthenticatedRequest } from '../../../context/loginContext';
36
 
36
 
37
 export type ClubCategoryType = {
37
 export type ClubCategoryType = {
38
   id: number;
38
   id: number;
49
   responsibles: Array<string>;
49
   responsibles: Array<string>;
50
 };
50
 };
51
 
51
 
52
-type PropsType = {
53
-  navigation: StackNavigationProp<any>;
54
-};
55
-
56
-type StateType = {
57
-  currentlySelectedCategories: Array<number>;
58
-  currentSearchString: string;
59
-};
60
-
61
 type ResponseType = {
52
 type ResponseType = {
62
   categories: Array<ClubCategoryType>;
53
   categories: Array<ClubCategoryType>;
63
   clubs: Array<ClubType>;
54
   clubs: Array<ClubType>;
65
 
56
 
66
 const LIST_ITEM_HEIGHT = 96;
57
 const LIST_ITEM_HEIGHT = 96;
67
 
58
 
68
-class ClubListScreen extends React.Component<PropsType, StateType> {
69
-  categories: Array<ClubCategoryType>;
70
-
71
-  constructor(props: PropsType) {
72
-    super(props);
73
-    this.categories = [];
74
-    this.state = {
75
-      currentlySelectedCategories: [],
76
-      currentSearchString: '',
59
+function ClubListScreen() {
60
+  const navigation = useNavigation();
61
+  const request = useAuthenticatedRequest<ResponseType>('clubs/list');
62
+  const [
63
+    currentlySelectedCategories,
64
+    setCurrentlySelectedCategories,
65
+  ] = useState<Array<number>>([]);
66
+  const [currentSearchString, setCurrentSearchString] = useState('');
67
+  const categories = useRef<Array<ClubCategoryType>>([]);
68
+
69
+  useLayoutEffect(() => {
70
+    const getSearchBar = () => {
71
+      return (
72
+        // @ts-ignore
73
+        <Searchbar
74
+          placeholder={i18n.t('screens.proximo.search')}
75
+          onChangeText={onSearchStringChange}
76
+        />
77
+      );
77
     };
78
     };
78
-  }
79
-
80
-  /**
81
-   * Creates the header content
82
-   */
83
-  componentDidMount() {
84
-    const { props } = this;
85
-    props.navigation.setOptions({
86
-      headerTitle: this.getSearchBar,
87
-      headerRight: this.getHeaderButtons,
79
+    const getHeaderButtons = () => {
80
+      return (
81
+        <MaterialHeaderButtons>
82
+          <Item
83
+            title="main"
84
+            iconName="information"
85
+            onPress={() => navigation.navigate('club-about')}
86
+          />
87
+        </MaterialHeaderButtons>
88
+      );
89
+    };
90
+    navigation.setOptions({
91
+      headerTitle: getSearchBar,
92
+      headerRight: getHeaderButtons,
88
       headerBackTitleVisible: false,
93
       headerBackTitleVisible: false,
89
       headerTitleContainerStyle:
94
       headerTitleContainerStyle:
90
         Platform.OS === 'ios'
95
         Platform.OS === 'ios'
91
           ? { marginHorizontal: 0, width: '70%' }
96
           ? { marginHorizontal: 0, width: '70%' }
92
           : { marginHorizontal: 0, right: 50, left: 50 },
97
           : { marginHorizontal: 0, right: 50, left: 50 },
93
     });
98
     });
94
-  }
99
+    // eslint-disable-next-line react-hooks/exhaustive-deps
100
+  }, [navigation]);
101
+
102
+  const onSearchStringChange = (str: string) => {
103
+    updateFilteredData(str, null);
104
+  };
95
 
105
 
96
   /**
106
   /**
97
    * Callback used when clicking an article in the list.
107
    * Callback used when clicking an article in the list.
99
    *
109
    *
100
    * @param item The article pressed
110
    * @param item The article pressed
101
    */
111
    */
102
-  onListItemPress(item: ClubType) {
103
-    const { props } = this;
104
-    props.navigation.navigate('club-information', {
112
+  const onListItemPress = (item: ClubType) => {
113
+    navigation.navigate('club-information', {
105
       data: item,
114
       data: item,
106
-      categories: this.categories,
115
+      categories: categories.current,
107
     });
116
     });
108
-  }
109
-
110
-  /**
111
-   * Callback used when the search changes
112
-   *
113
-   * @param str The new search string
114
-   */
115
-  onSearchStringChange = (str: string) => {
116
-    this.updateFilteredData(str, null);
117
-  };
118
-
119
-  /**
120
-   * Gets the header search bar
121
-   *
122
-   * @return {*}
123
-   */
124
-  getSearchBar = () => {
125
-    return (
126
-      // @ts-ignore
127
-      <Searchbar
128
-        placeholder={i18n.t('screens.proximo.search')}
129
-        onChangeText={this.onSearchStringChange}
130
-      />
131
-    );
132
   };
117
   };
133
 
118
 
134
-  onChipSelect = (id: number) => {
135
-    this.updateFilteredData(null, id);
119
+  const onChipSelect = (id: number) => {
120
+    updateFilteredData(null, id);
136
   };
121
   };
137
 
122
 
138
-  /**
139
-   * Gets the header button
140
-   * @return {*}
141
-   */
142
-  getHeaderButtons = () => {
143
-    const onPress = () => {
144
-      const { props } = this;
145
-      props.navigation.navigate('club-about');
146
-    };
147
-    return (
148
-      <MaterialHeaderButtons>
149
-        <Item title="main" iconName="information" onPress={onPress} />
150
-      </MaterialHeaderButtons>
151
-    );
152
-  };
153
-
154
-  createDataset = (data: ResponseType | undefined) => {
123
+  const createDataset = (data: ResponseType | undefined) => {
155
     if (data) {
124
     if (data) {
156
-      this.categories = data?.categories;
125
+      categories.current = data.categories;
157
       return [{ title: '', data: data.clubs }];
126
       return [{ title: '', data: data.clubs }];
158
     } else {
127
     } else {
159
       return [];
128
       return [];
165
    *
134
    *
166
    * @returns {*}
135
    * @returns {*}
167
    */
136
    */
168
-  getListHeader(data: ResponseType | undefined) {
169
-    const { state } = this;
137
+  const getListHeader = (data: ResponseType | undefined) => {
170
     if (data) {
138
     if (data) {
171
       return (
139
       return (
172
         <ClubListHeader
140
         <ClubListHeader
173
-          categories={this.categories}
174
-          selectedCategories={state.currentlySelectedCategories}
175
-          onChipSelect={this.onChipSelect}
141
+          categories={categories.current}
142
+          selectedCategories={currentlySelectedCategories}
143
+          onChipSelect={onChipSelect}
176
         />
144
         />
177
       );
145
       );
178
     } else {
146
     } else {
179
       return null;
147
       return null;
180
     }
148
     }
181
-  }
149
+  };
182
 
150
 
183
-  /**
184
-   * Gets the category object of the given ID
185
-   *
186
-   * @param id The ID of the category to find
187
-   * @returns {*}
188
-   */
189
-  getCategoryOfId = (id: number): ClubCategoryType | null => {
151
+  const getCategoryOfId = (id: number): ClubCategoryType | null => {
190
     let cat = null;
152
     let cat = null;
191
-    this.categories.forEach((item: ClubCategoryType) => {
153
+    categories.current.forEach((item: ClubCategoryType) => {
192
       if (id === item.id) {
154
       if (id === item.id) {
193
         cat = item;
155
         cat = item;
194
       }
156
       }
196
     return cat;
158
     return cat;
197
   };
159
   };
198
 
160
 
199
-  getRenderItem = ({ item }: { item: ClubType }) => {
161
+  const getRenderItem = ({ item }: { item: ClubType }) => {
200
     const onPress = () => {
162
     const onPress = () => {
201
-      this.onListItemPress(item);
163
+      onListItemPress(item);
202
     };
164
     };
203
-    if (this.shouldRenderItem(item)) {
165
+    if (shouldRenderItem(item)) {
204
       return (
166
       return (
205
         <ClubListItem
167
         <ClubListItem
206
-          categoryTranslator={this.getCategoryOfId}
168
+          categoryTranslator={getCategoryOfId}
207
           item={item}
169
           item={item}
208
           onPress={onPress}
170
           onPress={onPress}
209
           height={LIST_ITEM_HEIGHT}
171
           height={LIST_ITEM_HEIGHT}
213
     return null;
175
     return null;
214
   };
176
   };
215
 
177
 
216
-  keyExtractor = (item: ClubType): string => item.id.toString();
178
+  const keyExtractor = (item: ClubType): string => item.id.toString();
217
 
179
 
218
   /**
180
   /**
219
    * Updates the search string and category filter, saving them to the State.
181
    * Updates the search string and category filter, saving them to the State.
224
    * @param filterStr The new filter string to use
186
    * @param filterStr The new filter string to use
225
    * @param categoryId The category to add/remove from the filter
187
    * @param categoryId The category to add/remove from the filter
226
    */
188
    */
227
-  updateFilteredData(filterStr: string | null, categoryId: number | null) {
228
-    const { state } = this;
229
-    const newCategoriesState = [...state.currentlySelectedCategories];
230
-    let newStrState = state.currentSearchString;
189
+  const updateFilteredData = (
190
+    filterStr: string | null,
191
+    categoryId: number | null
192
+  ) => {
193
+    const newCategoriesState = [...currentlySelectedCategories];
194
+    let newStrState = currentSearchString;
231
     if (filterStr !== null) {
195
     if (filterStr !== null) {
232
       newStrState = filterStr;
196
       newStrState = filterStr;
233
     }
197
     }
240
       }
204
       }
241
     }
205
     }
242
     if (filterStr !== null || categoryId !== null) {
206
     if (filterStr !== null || categoryId !== null) {
243
-      this.setState({
244
-        currentSearchString: newStrState,
245
-        currentlySelectedCategories: newCategoriesState,
246
-      });
207
+      setCurrentSearchString(newStrState);
208
+      setCurrentlySelectedCategories(newCategoriesState);
247
     }
209
     }
248
-  }
210
+  };
249
 
211
 
250
   /**
212
   /**
251
    * Checks if the given item should be rendered according to current name and category filters
213
    * Checks if the given item should be rendered according to current name and category filters
253
    * @param item The club to check
215
    * @param item The club to check
254
    * @returns {boolean}
216
    * @returns {boolean}
255
    */
217
    */
256
-  shouldRenderItem(item: ClubType): boolean {
257
-    const { state } = this;
218
+  const shouldRenderItem = (item: ClubType): boolean => {
258
     let shouldRender =
219
     let shouldRender =
259
-      state.currentlySelectedCategories.length === 0 ||
260
-      isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
220
+      currentlySelectedCategories.length === 0 ||
221
+      isItemInCategoryFilter(currentlySelectedCategories, item.category);
261
     if (shouldRender) {
222
     if (shouldRender) {
262
-      shouldRender = stringMatchQuery(item.name, state.currentSearchString);
223
+      shouldRender = stringMatchQuery(item.name, currentSearchString);
263
     }
224
     }
264
     return shouldRender;
225
     return shouldRender;
265
-  }
226
+  };
266
 
227
 
267
-  render() {
268
-    return (
269
-      <WebSectionList
270
-        request={() =>
271
-          ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
272
-            'clubs/list'
273
-          )
274
-        }
275
-        createDataset={this.createDataset}
276
-        keyExtractor={this.keyExtractor}
277
-        renderItem={this.getRenderItem}
278
-        renderListHeaderComponent={(data) => this.getListHeader(data)}
279
-        // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
280
-        removeClippedSubviews={true}
281
-        itemHeight={LIST_ITEM_HEIGHT}
282
-      />
283
-    );
284
-  }
228
+  return (
229
+    <WebSectionList
230
+      request={request}
231
+      createDataset={createDataset}
232
+      keyExtractor={keyExtractor}
233
+      renderItem={getRenderItem}
234
+      renderListHeaderComponent={getListHeader}
235
+      // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
236
+      removeClippedSubviews={true}
237
+      itemHeight={LIST_ITEM_HEIGHT}
238
+    />
239
+  );
285
 }
240
 }
286
 
241
 
287
 export default ClubListScreen;
242
 export default ClubListScreen;

+ 57
- 84
src/screens/Amicale/Equipment/EquipmentListScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
20
+import React, { useRef, useState } from 'react';
21
 import { StyleSheet, View } from 'react-native';
21
 import { StyleSheet, View } from 'react-native';
22
 import { Button } from 'react-native-paper';
22
 import { Button } from 'react-native-paper';
23
-import { StackNavigationProp } from '@react-navigation/stack';
24
 import i18n from 'i18n-js';
23
 import i18n from 'i18n-js';
25
 import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
24
 import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
26
 import MascotPopup from '../../../components/Mascot/MascotPopup';
25
 import MascotPopup from '../../../components/Mascot/MascotPopup';
27
 import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
26
 import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
28
 import GENERAL_STYLES from '../../../constants/Styles';
27
 import GENERAL_STYLES from '../../../constants/Styles';
29
-import ConnectionManager from '../../../managers/ConnectionManager';
30
 import { ApiRejectType } from '../../../utils/WebData';
28
 import { ApiRejectType } from '../../../utils/WebData';
31
 import WebSectionList from '../../../components/Screens/WebSectionList';
29
 import WebSectionList from '../../../components/Screens/WebSectionList';
32
-
33
-type PropsType = {
34
-  navigation: StackNavigationProp<any>;
35
-};
36
-
37
-type StateType = {
38
-  mascotDialogVisible: boolean | undefined;
39
-};
30
+import { useAuthenticatedRequest } from '../../../context/loginContext';
40
 
31
 
41
 export type DeviceType = {
32
 export type DeviceType = {
42
   id: number;
33
   id: number;
67
   },
58
   },
68
 });
59
 });
69
 
60
 
70
-class EquipmentListScreen extends React.Component<PropsType, StateType> {
71
-  userRents: null | Array<RentedDeviceType>;
61
+function EquipmentListScreen() {
62
+  const userRents = useRef<undefined | Array<RentedDeviceType>>();
63
+  const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
72
 
64
 
73
-  constructor(props: PropsType) {
74
-    super(props);
75
-    this.userRents = null;
76
-    this.state = {
77
-      mascotDialogVisible: undefined,
78
-    };
79
-  }
65
+  const requestAll = useAuthenticatedRequest<{ devices: Array<DeviceType> }>(
66
+    'location/all'
67
+  );
68
+  const requestOwn = useAuthenticatedRequest<{
69
+    locations: Array<RentedDeviceType>;
70
+  }>('location/my');
80
 
71
 
81
-  getRenderItem = ({ item }: { item: DeviceType }) => {
82
-    const { navigation } = this.props;
72
+  const getRenderItem = ({ item }: { item: DeviceType }) => {
83
     return (
73
     return (
84
       <EquipmentListItem
74
       <EquipmentListItem
85
-        navigation={navigation}
86
         item={item}
75
         item={item}
87
-        userDeviceRentDates={this.getUserDeviceRentDates(item)}
76
+        userDeviceRentDates={getUserDeviceRentDates(item)}
88
         height={LIST_ITEM_HEIGHT}
77
         height={LIST_ITEM_HEIGHT}
89
       />
78
       />
90
     );
79
     );
91
   };
80
   };
92
 
81
 
93
-  getUserDeviceRentDates(item: DeviceType): [string, string] | null {
82
+  const getUserDeviceRentDates = (
83
+    item: DeviceType
84
+  ): [string, string] | null => {
94
     let dates = null;
85
     let dates = null;
95
-    if (this.userRents != null) {
96
-      this.userRents.forEach((device: RentedDeviceType) => {
86
+    if (userRents.current) {
87
+      userRents.current.forEach((device: RentedDeviceType) => {
97
         if (item.id === device.device_id) {
88
         if (item.id === device.device_id) {
98
           dates = [device.begin, device.end];
89
           dates = [device.begin, device.end];
99
         }
90
         }
100
       });
91
       });
101
     }
92
     }
102
     return dates;
93
     return dates;
103
-  }
104
-
105
-  /**
106
-   * Gets the list header, with explains this screen's purpose
107
-   *
108
-   * @returns {*}
109
-   */
110
-  getListHeader() {
94
+  };
95
+
96
+  const getListHeader = () => {
111
     return (
97
     return (
112
       <View style={styles.headerContainer}>
98
       <View style={styles.headerContainer}>
113
         <Button
99
         <Button
114
           mode="contained"
100
           mode="contained"
115
           icon="help-circle"
101
           icon="help-circle"
116
-          onPress={this.showMascotDialog}
102
+          onPress={showMascotDialog}
117
           style={GENERAL_STYLES.centerHorizontal}
103
           style={GENERAL_STYLES.centerHorizontal}
118
         >
104
         >
119
           {i18n.t('screens.equipment.mascotDialog.title')}
105
           {i18n.t('screens.equipment.mascotDialog.title')}
120
         </Button>
106
         </Button>
121
       </View>
107
       </View>
122
     );
108
     );
123
-  }
109
+  };
124
 
110
 
125
-  keyExtractor = (item: DeviceType): string => item.id.toString();
111
+  const keyExtractor = (item: DeviceType): string => item.id.toString();
126
 
112
 
127
-  createDataset = (data: ResponseType | undefined) => {
113
+  const createDataset = (data: ResponseType | undefined) => {
128
     if (data) {
114
     if (data) {
129
-      const userRents = data.locations;
130
-
131
-      if (userRents) {
132
-        this.userRents = userRents;
115
+      if (data.locations) {
116
+        userRents.current = data.locations;
133
       }
117
       }
134
       return [{ title: '', data: data.devices }];
118
       return [{ title: '', data: data.devices }];
135
     } else {
119
     } else {
137
     }
121
     }
138
   };
122
   };
139
 
123
 
140
-  showMascotDialog = () => {
141
-    this.setState({ mascotDialogVisible: true });
142
-  };
124
+  const showMascotDialog = () => setMascotDialogVisible(true);
143
 
125
 
144
-  hideMascotDialog = () => {
145
-    this.setState({ mascotDialogVisible: false });
146
-  };
126
+  const hideMascotDialog = () => setMascotDialogVisible(false);
147
 
127
 
148
-  request = () => {
128
+  const request = () => {
149
     return new Promise(
129
     return new Promise(
150
       (
130
       (
151
         resolve: (data: ResponseType) => void,
131
         resolve: (data: ResponseType) => void,
152
         reject: (error: ApiRejectType) => void
132
         reject: (error: ApiRejectType) => void
153
       ) => {
133
       ) => {
154
-        ConnectionManager.getInstance()
155
-          .authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
134
+        requestAll()
156
           .then((devicesData) => {
135
           .then((devicesData) => {
157
-            ConnectionManager.getInstance()
158
-              .authenticatedRequest<{
159
-                locations: Array<RentedDeviceType>;
160
-              }>('location/my')
136
+            requestOwn()
161
               .then((rentsData) => {
137
               .then((rentsData) => {
162
                 resolve({
138
                 resolve({
163
                   devices: devicesData.devices,
139
                   devices: devicesData.devices,
175
     );
151
     );
176
   };
152
   };
177
 
153
 
178
-  render() {
179
-    const { state } = this;
180
-    return (
181
-      <View style={GENERAL_STYLES.flex}>
182
-        <WebSectionList
183
-          request={this.request}
184
-          createDataset={this.createDataset}
185
-          keyExtractor={this.keyExtractor}
186
-          renderItem={this.getRenderItem}
187
-          renderListHeaderComponent={() => this.getListHeader()}
188
-        />
189
-        <MascotPopup
190
-          visible={state.mascotDialogVisible}
191
-          title={i18n.t('screens.equipment.mascotDialog.title')}
192
-          message={i18n.t('screens.equipment.mascotDialog.message')}
193
-          icon="vote"
194
-          buttons={{
195
-            cancel: {
196
-              message: i18n.t('screens.equipment.mascotDialog.button'),
197
-              icon: 'check',
198
-              onPress: this.hideMascotDialog,
199
-            },
200
-          }}
201
-          emotion={MASCOT_STYLE.WINK}
202
-        />
203
-      </View>
204
-    );
205
-  }
154
+  return (
155
+    <View style={GENERAL_STYLES.flex}>
156
+      <WebSectionList
157
+        request={request}
158
+        createDataset={createDataset}
159
+        keyExtractor={keyExtractor}
160
+        renderItem={getRenderItem}
161
+        renderListHeaderComponent={getListHeader}
162
+      />
163
+      <MascotPopup
164
+        visible={mascotDialogVisible}
165
+        title={i18n.t('screens.equipment.mascotDialog.title')}
166
+        message={i18n.t('screens.equipment.mascotDialog.message')}
167
+        icon="vote"
168
+        buttons={{
169
+          cancel: {
170
+            message: i18n.t('screens.equipment.mascotDialog.button'),
171
+            icon: 'check',
172
+            onPress: hideMascotDialog,
173
+          },
174
+        }}
175
+        emotion={MASCOT_STYLE.WINK}
176
+      />
177
+    </View>
178
+  );
206
 }
179
 }
207
 
180
 
208
 export default EquipmentListScreen;
181
 export default EquipmentListScreen;

+ 248
- 302
src/screens/Amicale/Equipment/EquipmentRentScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
20
+import React, { useCallback, useRef, useState } from 'react';
21
 import {
21
 import {
22
   Button,
22
   Button,
23
   Caption,
23
   Caption,
24
   Card,
24
   Card,
25
   Headline,
25
   Headline,
26
   Subheading,
26
   Subheading,
27
-  withTheme,
27
+  useTheme,
28
 } from 'react-native-paper';
28
 } from 'react-native-paper';
29
 import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
29
 import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
30
 import { BackHandler, StyleSheet, View } from 'react-native';
30
 import { BackHandler, StyleSheet, View } from 'react-native';
31
 import * as Animatable from 'react-native-animatable';
31
 import * as Animatable from 'react-native-animatable';
32
 import i18n from 'i18n-js';
32
 import i18n from 'i18n-js';
33
 import { CalendarList, PeriodMarking } from 'react-native-calendars';
33
 import { CalendarList, PeriodMarking } from 'react-native-calendars';
34
-import type { DeviceType } from './EquipmentListScreen';
35
 import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
34
 import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
36
 import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
35
 import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
37
 import {
36
 import {
42
   getValidRange,
41
   getValidRange,
43
   isEquipmentAvailable,
42
   isEquipmentAvailable,
44
 } from '../../../utils/EquipmentBooking';
43
 } from '../../../utils/EquipmentBooking';
45
-import ConnectionManager from '../../../managers/ConnectionManager';
46
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
44
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
47
 import { MainStackParamsList } from '../../../navigation/MainNavigator';
45
 import { MainStackParamsList } from '../../../navigation/MainNavigator';
48
 import GENERAL_STYLES from '../../../constants/Styles';
46
 import GENERAL_STYLES from '../../../constants/Styles';
49
 import { ApiRejectType } from '../../../utils/WebData';
47
 import { ApiRejectType } from '../../../utils/WebData';
50
 import { REQUEST_STATUS } from '../../../utils/Requests';
48
 import { REQUEST_STATUS } from '../../../utils/Requests';
49
+import { useFocusEffect } from '@react-navigation/core';
50
+import { useNavigation } from '@react-navigation/native';
51
+import { useAuthenticatedRequest } from '../../../context/loginContext';
51
 
52
 
52
-type EquipmentRentScreenNavigationProp = StackScreenProps<
53
-  MainStackParamsList,
54
-  'equipment-rent'
55
->;
56
-
57
-type Props = EquipmentRentScreenNavigationProp & {
58
-  navigation: StackNavigationProp<any>;
59
-  theme: ReactNativePaper.Theme;
60
-};
53
+type Props = StackScreenProps<MainStackParamsList, 'equipment-rent'>;
61
 
54
 
62
 export type MarkedDatesObjectType = {
55
 export type MarkedDatesObjectType = {
63
   [key: string]: PeriodMarking;
56
   [key: string]: PeriodMarking;
64
 };
57
 };
65
 
58
 
66
-type StateType = {
67
-  dialogVisible: boolean;
68
-  errorDialogVisible: boolean;
69
-  markedDates: MarkedDatesObjectType;
70
-  currentError: ApiRejectType;
71
-};
72
-
73
 const styles = StyleSheet.create({
59
 const styles = StyleSheet.create({
74
   titleContainer: {
60
   titleContainer: {
75
     marginLeft: 'auto',
61
     marginLeft: 'auto',
114
   },
100
   },
115
 });
101
 });
116
 
102
 
117
-class EquipmentRentScreen extends React.Component<Props, StateType> {
118
-  item: DeviceType | null;
103
+function EquipmentRentScreen(props: Props) {
104
+  const theme = useTheme();
105
+  const navigation = useNavigation<StackNavigationProp<any>>();
106
+  const [currentError, setCurrentError] = useState<ApiRejectType>({
107
+    status: REQUEST_STATUS.SUCCESS,
108
+  });
109
+  const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({});
110
+  const [dialogVisible, setDialogVisible] = useState(false);
119
 
111
 
120
-  bookedDates: Array<string>;
112
+  const item = props.route.params.item;
121
 
113
 
122
-  bookRef: { current: null | (Animatable.View & View) };
114
+  const bookedDates = useRef<Array<string>>([]);
115
+  const canBookEquipment = useRef(false);
123
 
116
 
124
-  canBookEquipment: boolean;
117
+  const bookRef = useRef<Animatable.View & View>(null);
125
 
118
 
126
-  lockedDates: {
119
+  let lockedDates: {
127
     [key: string]: PeriodMarking;
120
     [key: string]: PeriodMarking;
128
-  };
129
-
130
-  constructor(props: Props) {
131
-    super(props);
132
-    this.item = null;
133
-    this.lockedDates = {};
134
-    this.state = {
135
-      dialogVisible: false,
136
-      errorDialogVisible: false,
137
-      markedDates: {},
138
-      currentError: { status: REQUEST_STATUS.SUCCESS },
139
-    };
140
-    this.resetSelection();
141
-    this.bookRef = React.createRef();
142
-    this.canBookEquipment = false;
143
-    this.bookedDates = [];
144
-    if (props.route.params != null) {
145
-      if (props.route.params.item != null) {
146
-        this.item = props.route.params.item;
147
-      } else {
148
-        this.item = null;
149
-      }
150
-    }
151
-    const { item } = this;
152
-    if (item != null) {
153
-      this.lockedDates = {};
154
-      item.booked_at.forEach((date: { begin: string; end: string }) => {
155
-        const range = getValidRange(
156
-          new Date(date.begin),
157
-          new Date(date.end),
158
-          null
159
-        );
160
-        this.lockedDates = {
161
-          ...this.lockedDates,
162
-          ...generateMarkedDates(false, props.theme, range),
163
-        };
164
-      });
165
-    }
121
+  } = {};
122
+
123
+  if (item) {
124
+    item.booked_at.forEach((date: { begin: string; end: string }) => {
125
+      const range = getValidRange(
126
+        new Date(date.begin),
127
+        new Date(date.end),
128
+        null
129
+      );
130
+      lockedDates = {
131
+        ...lockedDates,
132
+        ...generateMarkedDates(false, theme, range),
133
+      };
134
+    });
166
   }
135
   }
167
 
136
 
168
-  /**
169
-   * Captures focus and blur events to hook on android back button
170
-   */
171
-  componentDidMount() {
172
-    const { navigation } = this.props;
173
-    navigation.addListener('focus', () => {
137
+  useFocusEffect(
138
+    useCallback(() => {
174
       BackHandler.addEventListener(
139
       BackHandler.addEventListener(
175
         'hardwareBackPress',
140
         'hardwareBackPress',
176
-        this.onBackButtonPressAndroid
177
-      );
178
-    });
179
-    navigation.addListener('blur', () => {
180
-      BackHandler.removeEventListener(
181
-        'hardwareBackPress',
182
-        this.onBackButtonPressAndroid
141
+        onBackButtonPressAndroid
183
       );
142
       );
184
-    });
185
-  }
143
+      return () => {
144
+        BackHandler.removeEventListener(
145
+          'hardwareBackPress',
146
+          onBackButtonPressAndroid
147
+        );
148
+      };
149
+      // eslint-disable-next-line react-hooks/exhaustive-deps
150
+    }, [])
151
+  );
186
 
152
 
187
   /**
153
   /**
188
    * Overrides default android back button behaviour to deselect date if any is selected.
154
    * Overrides default android back button behaviour to deselect date if any is selected.
189
    *
155
    *
190
    * @return {boolean}
156
    * @return {boolean}
191
    */
157
    */
192
-  onBackButtonPressAndroid = (): boolean => {
193
-    if (this.bookedDates.length > 0) {
194
-      this.resetSelection();
195
-      this.updateMarkedSelection();
158
+  const onBackButtonPressAndroid = (): boolean => {
159
+    if (bookedDates.current.length > 0) {
160
+      resetSelection();
161
+      updateMarkedSelection();
196
       return true;
162
       return true;
197
     }
163
     }
198
     return false;
164
     return false;
199
   };
165
   };
200
 
166
 
201
-  onDialogDismiss = () => {
202
-    this.setState({ dialogVisible: false });
167
+  const showDialog = () => setDialogVisible(true);
168
+
169
+  const onDialogDismiss = () => setDialogVisible(false);
170
+
171
+  const onErrorDialogDismiss = () =>
172
+    setCurrentError({ status: REQUEST_STATUS.SUCCESS });
173
+
174
+  const getBookStartDate = (): Date | null => {
175
+    return bookedDates.current.length > 0
176
+      ? new Date(bookedDates.current[0])
177
+      : null;
203
   };
178
   };
204
 
179
 
205
-  onErrorDialogDismiss = () => {
206
-    this.setState({ errorDialogVisible: false });
180
+  const getBookEndDate = (): Date | null => {
181
+    const { length } = bookedDates.current;
182
+    return length > 0 ? new Date(bookedDates.current[length - 1]) : null;
207
   };
183
   };
208
 
184
 
185
+  const start = getBookStartDate();
186
+  const end = getBookEndDate();
187
+  const request = useAuthenticatedRequest(
188
+    'location/booking',
189
+    item && start && end
190
+      ? {
191
+          device: item.id,
192
+          begin: getISODate(start),
193
+          end: getISODate(end),
194
+        }
195
+      : undefined
196
+  );
197
+
209
   /**
198
   /**
210
    * Sends the selected data to the server and waits for a response.
199
    * Sends the selected data to the server and waits for a response.
211
    * If the request is a success, navigate to the recap screen.
200
    * If the request is a success, navigate to the recap screen.
213
    *
202
    *
214
    * @returns {Promise<void>}
203
    * @returns {Promise<void>}
215
    */
204
    */
216
-  onDialogAccept = (): Promise<void> => {
205
+  const onDialogAccept = (): Promise<void> => {
217
     return new Promise((resolve: () => void) => {
206
     return new Promise((resolve: () => void) => {
218
-      const { item, props } = this;
219
-      const start = this.getBookStartDate();
220
-      const end = this.getBookEndDate();
221
       if (item != null && start != null && end != null) {
207
       if (item != null && start != null && end != null) {
222
-        ConnectionManager.getInstance()
223
-          .authenticatedRequest('location/booking', {
224
-            device: item.id,
225
-            begin: getISODate(start),
226
-            end: getISODate(end),
227
-          })
208
+        request()
228
           .then(() => {
209
           .then(() => {
229
-            this.onDialogDismiss();
230
-            props.navigation.replace('equipment-confirm', {
231
-              item: this.item,
210
+            onDialogDismiss();
211
+            navigation.replace('equipment-confirm', {
212
+              item: item,
232
               dates: [getISODate(start), getISODate(end)],
213
               dates: [getISODate(start), getISODate(end)],
233
             });
214
             });
234
             resolve();
215
             resolve();
235
           })
216
           })
236
           .catch((error: ApiRejectType) => {
217
           .catch((error: ApiRejectType) => {
237
-            this.onDialogDismiss();
238
-            this.showErrorDialog(error);
218
+            onDialogDismiss();
219
+            setCurrentError(error);
239
             resolve();
220
             resolve();
240
           });
221
           });
241
       } else {
222
       } else {
242
-        this.onDialogDismiss();
223
+        onDialogDismiss();
243
         resolve();
224
         resolve();
244
       }
225
       }
245
     });
226
     });
246
   };
227
   };
247
 
228
 
248
-  getBookStartDate(): Date | null {
249
-    return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
250
-  }
251
-
252
-  getBookEndDate(): Date | null {
253
-    const { length } = this.bookedDates;
254
-    return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
255
-  }
256
-
257
   /**
229
   /**
258
    * Selects a new date on the calendar.
230
    * Selects a new date on the calendar.
259
    * If both start and end dates are already selected, unselect all.
231
    * If both start and end dates are already selected, unselect all.
260
    *
232
    *
261
    * @param day The day selected
233
    * @param day The day selected
262
    */
234
    */
263
-  selectNewDate = (day: {
235
+  const selectNewDate = (day: {
264
     dateString: string;
236
     dateString: string;
265
     day: number;
237
     day: number;
266
     month: number;
238
     month: number;
268
     year: number;
240
     year: number;
269
   }) => {
241
   }) => {
270
     const selected = new Date(day.dateString);
242
     const selected = new Date(day.dateString);
271
-    const start = this.getBookStartDate();
272
 
243
 
273
-    if (!this.lockedDates[day.dateString] != null) {
244
+    if (!lockedDates[day.dateString] != null) {
274
       if (start === null) {
245
       if (start === null) {
275
-        this.updateSelectionRange(selected, selected);
276
-        this.enableBooking();
246
+        updateSelectionRange(selected, selected);
247
+        enableBooking();
277
       } else if (start.getTime() === selected.getTime()) {
248
       } else if (start.getTime() === selected.getTime()) {
278
-        this.resetSelection();
279
-      } else if (this.bookedDates.length === 1) {
280
-        this.updateSelectionRange(start, selected);
281
-        this.enableBooking();
249
+        resetSelection();
250
+      } else if (bookedDates.current.length === 1) {
251
+        updateSelectionRange(start, selected);
252
+        enableBooking();
282
       } else {
253
       } else {
283
-        this.resetSelection();
254
+        resetSelection();
284
       }
255
       }
285
-      this.updateMarkedSelection();
256
+      updateMarkedSelection();
286
     }
257
     }
287
   };
258
   };
288
 
259
 
289
-  showErrorDialog = (error: ApiRejectType) => {
290
-    this.setState({
291
-      errorDialogVisible: true,
292
-      currentError: error,
293
-    });
260
+  const showBookButton = () => {
261
+    if (bookRef.current && bookRef.current.fadeInUp) {
262
+      bookRef.current.fadeInUp(500);
263
+    }
294
   };
264
   };
295
 
265
 
296
-  showDialog = () => {
297
-    this.setState({ dialogVisible: true });
266
+  const hideBookButton = () => {
267
+    if (bookRef.current && bookRef.current.fadeOutDown) {
268
+      bookRef.current.fadeOutDown(500);
269
+    }
298
   };
270
   };
299
 
271
 
300
-  /**
301
-   * Shows the book button by plying a fade animation
302
-   */
303
-  showBookButton() {
304
-    if (this.bookRef.current && this.bookRef.current.fadeInUp) {
305
-      this.bookRef.current.fadeInUp(500);
272
+  const enableBooking = () => {
273
+    if (!canBookEquipment.current) {
274
+      showBookButton();
275
+      canBookEquipment.current = true;
306
     }
276
     }
307
-  }
277
+  };
308
 
278
 
309
-  /**
310
-   * Hides the book button by plying a fade animation
311
-   */
312
-  hideBookButton() {
313
-    if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
314
-      this.bookRef.current.fadeOutDown(500);
279
+  const resetSelection = () => {
280
+    if (canBookEquipment.current) {
281
+      hideBookButton();
315
     }
282
     }
316
-  }
283
+    canBookEquipment.current = false;
284
+    bookedDates.current = [];
285
+  };
317
 
286
 
318
-  enableBooking() {
319
-    if (!this.canBookEquipment) {
320
-      this.showBookButton();
321
-      this.canBookEquipment = true;
287
+  const updateSelectionRange = (s: Date, e: Date) => {
288
+    if (item) {
289
+      bookedDates.current = getValidRange(s, e, item);
290
+    } else {
291
+      bookedDates.current = [];
322
     }
292
     }
323
-  }
293
+  };
324
 
294
 
325
-  resetSelection() {
326
-    if (this.canBookEquipment) {
327
-      this.hideBookButton();
328
-    }
329
-    this.canBookEquipment = false;
330
-    this.bookedDates = [];
331
-  }
295
+  const updateMarkedSelection = () => {
296
+    setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
297
+  };
332
 
298
 
333
-  updateSelectionRange(start: Date, end: Date) {
334
-    this.bookedDates = getValidRange(start, end, this.item);
335
-  }
299
+  let subHeadingText;
336
 
300
 
337
-  updateMarkedSelection() {
338
-    const { theme } = this.props;
339
-    this.setState({
340
-      markedDates: generateMarkedDates(true, theme, this.bookedDates),
301
+  if (start == null) {
302
+    subHeadingText = i18n.t('screens.equipment.booking');
303
+  } else if (end != null && start.getTime() !== end.getTime()) {
304
+    subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
305
+      begin: getRelativeDateString(start),
306
+      end: getRelativeDateString(end),
307
+    });
308
+  } else {
309
+    subHeadingText = i18n.t('screens.equipment.bookingDay', {
310
+      date: getRelativeDateString(start),
341
     });
311
     });
342
   }
312
   }
343
 
313
 
344
-  render() {
345
-    const { item, props, state } = this;
346
-    const start = this.getBookStartDate();
347
-    const end = this.getBookEndDate();
348
-    let subHeadingText;
349
-    if (start == null) {
350
-      subHeadingText = i18n.t('screens.equipment.booking');
351
-    } else if (end != null && start.getTime() !== end.getTime()) {
352
-      subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
353
-        begin: getRelativeDateString(start),
354
-        end: getRelativeDateString(end),
355
-      });
356
-    } else {
357
-      subHeadingText = i18n.t('screens.equipment.bookingDay', {
358
-        date: getRelativeDateString(start),
359
-      });
360
-    }
361
-    if (item != null) {
362
-      const isAvailable = isEquipmentAvailable(item);
363
-      const firstAvailability = getFirstEquipmentAvailability(item);
364
-      return (
365
-        <View style={GENERAL_STYLES.flex}>
366
-          <CollapsibleScrollView>
367
-            <Card style={styles.card}>
368
-              <Card.Content>
369
-                <View style={GENERAL_STYLES.flex}>
370
-                  <View style={styles.titleContainer}>
371
-                    <Headline style={styles.title}>{item.name}</Headline>
372
-                    <Caption style={styles.caption}>
373
-                      (
374
-                      {i18n.t('screens.equipment.bail', { cost: item.caution })}
375
-                      )
376
-                    </Caption>
377
-                  </View>
314
+  if (item) {
315
+    const isAvailable = isEquipmentAvailable(item);
316
+    const firstAvailability = getFirstEquipmentAvailability(item);
317
+    return (
318
+      <View style={GENERAL_STYLES.flex}>
319
+        <CollapsibleScrollView>
320
+          <Card style={styles.card}>
321
+            <Card.Content>
322
+              <View style={GENERAL_STYLES.flex}>
323
+                <View style={styles.titleContainer}>
324
+                  <Headline style={styles.title}>{item.name}</Headline>
325
+                  <Caption style={styles.caption}>
326
+                    ({i18n.t('screens.equipment.bail', { cost: item.caution })})
327
+                  </Caption>
378
                 </View>
328
                 </View>
379
-
380
-                <Button
381
-                  icon={isAvailable ? 'check-circle-outline' : 'update'}
382
-                  color={
383
-                    isAvailable
384
-                      ? props.theme.colors.success
385
-                      : props.theme.colors.primary
386
-                  }
387
-                  mode="text"
388
-                >
389
-                  {i18n.t('screens.equipment.available', {
390
-                    date: getRelativeDateString(firstAvailability),
391
-                  })}
392
-                </Button>
393
-                <Subheading style={styles.subtitle}>
394
-                  {subHeadingText}
395
-                </Subheading>
396
-              </Card.Content>
397
-            </Card>
398
-            <CalendarList
399
-              // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
400
-              minDate={new Date()}
401
-              // Max amount of months allowed to scroll to the past. Default = 50
402
-              pastScrollRange={0}
403
-              // Max amount of months allowed to scroll to the future. Default = 50
404
-              futureScrollRange={3}
405
-              // Enable horizontal scrolling, default = false
406
-              horizontal
407
-              // Enable paging on horizontal, default = false
408
-              pagingEnabled
409
-              // Handler which gets executed on day press. Default = undefined
410
-              onDayPress={this.selectNewDate}
411
-              // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
412
-              firstDay={1}
413
-              // Hide month navigation arrows.
414
-              hideArrows={false}
415
-              // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
416
-              markingType={'period'}
417
-              markedDates={{ ...this.lockedDates, ...state.markedDates }}
418
-              theme={{
419
-                'backgroundColor': props.theme.colors.agendaBackgroundColor,
420
-                'calendarBackground': props.theme.colors.background,
421
-                'textSectionTitleColor': props.theme.colors.agendaDayTextColor,
422
-                'selectedDayBackgroundColor': props.theme.colors.primary,
423
-                'selectedDayTextColor': '#ffffff',
424
-                'todayTextColor': props.theme.colors.text,
425
-                'dayTextColor': props.theme.colors.text,
426
-                'textDisabledColor': props.theme.colors.agendaDayTextColor,
427
-                'dotColor': props.theme.colors.primary,
428
-                'selectedDotColor': '#ffffff',
429
-                'arrowColor': props.theme.colors.primary,
430
-                'monthTextColor': props.theme.colors.text,
431
-                'indicatorColor': props.theme.colors.primary,
432
-                'textDayFontFamily': 'monospace',
433
-                'textMonthFontFamily': 'monospace',
434
-                'textDayHeaderFontFamily': 'monospace',
435
-                'textDayFontWeight': '300',
436
-                'textMonthFontWeight': 'bold',
437
-                'textDayHeaderFontWeight': '300',
438
-                'textDayFontSize': 16,
439
-                'textMonthFontSize': 16,
440
-                'textDayHeaderFontSize': 16,
441
-                'stylesheet.day.period': {
442
-                  base: {
443
-                    overflow: 'hidden',
444
-                    height: 34,
445
-                    width: 34,
446
-                    alignItems: 'center',
447
-                  },
329
+              </View>
330
+
331
+              <Button
332
+                icon={isAvailable ? 'check-circle-outline' : 'update'}
333
+                color={
334
+                  isAvailable ? theme.colors.success : theme.colors.primary
335
+                }
336
+                mode="text"
337
+              >
338
+                {i18n.t('screens.equipment.available', {
339
+                  date: getRelativeDateString(firstAvailability),
340
+                })}
341
+              </Button>
342
+              <Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
343
+            </Card.Content>
344
+          </Card>
345
+          <CalendarList
346
+            // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
347
+            minDate={new Date()}
348
+            // Max amount of months allowed to scroll to the past. Default = 50
349
+            pastScrollRange={0}
350
+            // Max amount of months allowed to scroll to the future. Default = 50
351
+            futureScrollRange={3}
352
+            // Enable horizontal scrolling, default = false
353
+            horizontal
354
+            // Enable paging on horizontal, default = false
355
+            pagingEnabled
356
+            // Handler which gets executed on day press. Default = undefined
357
+            onDayPress={selectNewDate}
358
+            // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
359
+            firstDay={1}
360
+            // Hide month navigation arrows.
361
+            hideArrows={false}
362
+            // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
363
+            markingType={'period'}
364
+            markedDates={{ ...lockedDates, ...markedDates }}
365
+            theme={{
366
+              'backgroundColor': theme.colors.agendaBackgroundColor,
367
+              'calendarBackground': theme.colors.background,
368
+              'textSectionTitleColor': theme.colors.agendaDayTextColor,
369
+              'selectedDayBackgroundColor': theme.colors.primary,
370
+              'selectedDayTextColor': '#ffffff',
371
+              'todayTextColor': theme.colors.text,
372
+              'dayTextColor': theme.colors.text,
373
+              'textDisabledColor': theme.colors.agendaDayTextColor,
374
+              'dotColor': theme.colors.primary,
375
+              'selectedDotColor': '#ffffff',
376
+              'arrowColor': theme.colors.primary,
377
+              'monthTextColor': theme.colors.text,
378
+              'indicatorColor': theme.colors.primary,
379
+              'textDayFontFamily': 'monospace',
380
+              'textMonthFontFamily': 'monospace',
381
+              'textDayHeaderFontFamily': 'monospace',
382
+              'textDayFontWeight': '300',
383
+              'textMonthFontWeight': 'bold',
384
+              'textDayHeaderFontWeight': '300',
385
+              'textDayFontSize': 16,
386
+              'textMonthFontSize': 16,
387
+              'textDayHeaderFontSize': 16,
388
+              'stylesheet.day.period': {
389
+                base: {
390
+                  overflow: 'hidden',
391
+                  height: 34,
392
+                  width: 34,
393
+                  alignItems: 'center',
448
                 },
394
                 },
449
-              }}
450
-              style={styles.calendar}
451
-            />
452
-          </CollapsibleScrollView>
453
-          <LoadingConfirmDialog
454
-            visible={state.dialogVisible}
455
-            onDismiss={this.onDialogDismiss}
456
-            onAccept={this.onDialogAccept}
457
-            title={i18n.t('screens.equipment.dialogTitle')}
458
-            titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
459
-            message={i18n.t('screens.equipment.dialogMessage')}
395
+              },
396
+            }}
397
+            style={styles.calendar}
460
           />
398
           />
461
-
462
-          <ErrorDialog
463
-            visible={state.errorDialogVisible}
464
-            onDismiss={this.onErrorDialogDismiss}
465
-            status={state.currentError.status}
466
-            code={state.currentError.code}
467
-          />
468
-          <Animatable.View
469
-            ref={this.bookRef}
470
-            useNativeDriver
471
-            style={styles.buttonContainer}
399
+        </CollapsibleScrollView>
400
+        <LoadingConfirmDialog
401
+          visible={dialogVisible}
402
+          onDismiss={onDialogDismiss}
403
+          onAccept={onDialogAccept}
404
+          title={i18n.t('screens.equipment.dialogTitle')}
405
+          titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
406
+          message={i18n.t('screens.equipment.dialogMessage')}
407
+        />
408
+
409
+        <ErrorDialog
410
+          visible={currentError.status !== REQUEST_STATUS.SUCCESS}
411
+          onDismiss={onErrorDialogDismiss}
412
+          status={currentError.status}
413
+          code={currentError.code}
414
+        />
415
+        <Animatable.View
416
+          ref={bookRef}
417
+          useNativeDriver
418
+          style={styles.buttonContainer}
419
+        >
420
+          <Button
421
+            icon="bookmark-check"
422
+            mode="contained"
423
+            onPress={showDialog}
424
+            style={styles.button}
472
           >
425
           >
473
-            <Button
474
-              icon="bookmark-check"
475
-              mode="contained"
476
-              onPress={this.showDialog}
477
-              style={styles.button}
478
-            >
479
-              {i18n.t('screens.equipment.bookButton')}
480
-            </Button>
481
-          </Animatable.View>
482
-        </View>
483
-      );
484
-    }
485
-    return null;
426
+            {i18n.t('screens.equipment.bookButton')}
427
+          </Button>
428
+        </Animatable.View>
429
+      </View>
430
+    );
486
   }
431
   }
432
+  return null;
487
 }
433
 }
488
 
434
 
489
-export default withTheme(EquipmentRentScreen);
435
+export default EquipmentRentScreen;

+ 88
- 405
src/screens/Amicale/LoginScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
21
-import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
22
-import {
23
-  Button,
24
-  Card,
25
-  HelperText,
26
-  TextInput,
27
-  withTheme,
28
-} from 'react-native-paper';
20
+import React, { useCallback, useState } from 'react';
21
+import { KeyboardAvoidingView, View } from 'react-native';
29
 import i18n from 'i18n-js';
22
 import i18n from 'i18n-js';
30
 import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
23
 import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
31
 import LinearGradient from 'react-native-linear-gradient';
24
 import LinearGradient from 'react-native-linear-gradient';
32
-import ConnectionManager from '../../managers/ConnectionManager';
33
 import ErrorDialog from '../../components/Dialogs/ErrorDialog';
25
 import ErrorDialog from '../../components/Dialogs/ErrorDialog';
34
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
26
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
35
 import MascotPopup from '../../components/Mascot/MascotPopup';
27
 import MascotPopup from '../../components/Mascot/MascotPopup';
37
 import { MainStackParamsList } from '../../navigation/MainNavigator';
29
 import { MainStackParamsList } from '../../navigation/MainNavigator';
38
 import GENERAL_STYLES from '../../constants/Styles';
30
 import GENERAL_STYLES from '../../constants/Styles';
39
 import Urls from '../../constants/Urls';
31
 import Urls from '../../constants/Urls';
40
-import { ApiRejectType } from '../../utils/WebData';
32
+import { ApiRejectType, connectToAmicale } from '../../utils/WebData';
41
 import { REQUEST_STATUS } from '../../utils/Requests';
33
 import { REQUEST_STATUS } from '../../utils/Requests';
42
-
43
-type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>;
44
-
45
-type Props = LoginScreenNavigationProp & {
46
-  navigation: StackNavigationProp<any>;
47
-  theme: ReactNativePaper.Theme;
48
-};
49
-
50
-type StateType = {
51
-  email: string;
52
-  password: string;
53
-  isEmailValidated: boolean;
54
-  isPasswordValidated: boolean;
55
-  loading: boolean;
56
-  dialogVisible: boolean;
57
-  dialogError: ApiRejectType;
58
-  mascotDialogVisible: boolean | undefined;
59
-};
60
-
61
-const ICON_AMICALE = require('../../../assets/amicale.png');
62
-
63
-const emailRegex = /^.+@.+\..+$/;
64
-
65
-const styles = StyleSheet.create({
66
-  card: {
67
-    marginTop: 'auto',
68
-    marginBottom: 'auto',
69
-  },
70
-  header: {
71
-    fontSize: 36,
72
-    marginBottom: 48,
73
-  },
74
-  text: {
75
-    color: '#ffffff',
76
-  },
77
-  buttonContainer: {
78
-    flexWrap: 'wrap',
79
-  },
80
-  lockButton: {
81
-    marginRight: 'auto',
82
-    marginBottom: 20,
83
-  },
84
-  sendButton: {
85
-    marginLeft: 'auto',
86
-  },
87
-});
88
-
89
-class LoginScreen extends React.Component<Props, StateType> {
90
-  onEmailChange: (value: string) => void;
91
-
92
-  onPasswordChange: (value: string) => void;
93
-
94
-  passwordInputRef: {
95
-    // @ts-ignore
96
-    current: null | TextInput;
97
-  };
98
-
99
-  nextScreen: string | null;
100
-
101
-  constructor(props: Props) {
102
-    super(props);
103
-    this.nextScreen = null;
104
-    this.passwordInputRef = React.createRef();
105
-    this.onEmailChange = (value: string) => {
106
-      this.onInputChange(true, value);
107
-    };
108
-    this.onPasswordChange = (value: string) => {
109
-      this.onInputChange(false, value);
110
-    };
111
-    props.navigation.addListener('focus', this.onScreenFocus);
112
-    this.state = {
113
-      email: '',
114
-      password: '',
115
-      isEmailValidated: false,
116
-      isPasswordValidated: false,
117
-      loading: false,
118
-      dialogVisible: false,
119
-      dialogError: { status: REQUEST_STATUS.SUCCESS },
120
-      mascotDialogVisible: undefined,
121
-    };
122
-  }
123
-
124
-  onScreenFocus = () => {
125
-    this.handleNavigationParams();
126
-  };
127
-
128
-  /**
129
-   * Navigates to the Amicale website screen with the reset password link as navigation parameters
130
-   */
131
-  onResetPasswordClick = () => {
132
-    const { navigation } = this.props;
34
+import LoginForm from '../../components/Amicale/Login/LoginForm';
35
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
36
+import { TabRoutes } from '../../navigation/TabNavigator';
37
+import { useShouldShowMascot } from '../../context/preferencesContext';
38
+
39
+type Props = StackScreenProps<MainStackParamsList, 'login'>;
40
+
41
+function LoginScreen(props: Props) {
42
+  const navigation = useNavigation<StackNavigationProp<any>>();
43
+  const [loading, setLoading] = useState(false);
44
+  const [nextScreen, setNextScreen] = useState<string | undefined>(undefined);
45
+  const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
46
+  const [currentError, setCurrentError] = useState<ApiRejectType>({
47
+    status: REQUEST_STATUS.SUCCESS,
48
+  });
49
+  const homeMascot = useShouldShowMascot(TabRoutes.Home);
50
+
51
+  useFocusEffect(
52
+    useCallback(() => {
53
+      setNextScreen(props.route.params?.nextScreen);
54
+    }, [props.route.params])
55
+  );
56
+
57
+  const onResetPasswordClick = () => {
133
     navigation.navigate('website', {
58
     navigation.navigate('website', {
134
       host: Urls.websites.amicale,
59
       host: Urls.websites.amicale,
135
       path: Urls.amicale.resetPassword,
60
       path: Urls.amicale.resetPassword,
138
   };
63
   };
139
 
64
 
140
   /**
65
   /**
141
-   * Called when the user input changes in the email or password field.
142
-   * This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
143
-   *
144
-   * @param isEmail True if the field is the email field
145
-   * @param value The new field value
146
-   */
147
-  onInputChange(isEmail: boolean, value: string) {
148
-    if (isEmail) {
149
-      this.setState({
150
-        email: value,
151
-        isEmailValidated: false,
152
-      });
153
-    } else {
154
-      this.setState({
155
-        password: value,
156
-        isPasswordValidated: false,
157
-      });
158
-    }
159
-  }
160
-
161
-  /**
162
-   * Focuses the password field when the email field is done
163
-   *
164
-   * @returns {*}
165
-   */
166
-  onEmailSubmit = () => {
167
-    if (this.passwordInputRef.current != null) {
168
-      this.passwordInputRef.current.focus();
169
-    }
170
-  };
171
-
172
-  /**
173
    * Called when the user clicks on login or finishes to type his password.
66
    * Called when the user clicks on login or finishes to type his password.
174
    *
67
    *
175
    * Checks if we should allow the user to login,
68
    * Checks if we should allow the user to login,
176
    * then makes the login request and enters a loading state until the request finishes
69
    * then makes the login request and enters a loading state until the request finishes
177
    *
70
    *
178
    */
71
    */
179
-  onSubmit = () => {
180
-    const { email, password } = this.state;
181
-    if (this.shouldEnableLogin()) {
182
-      this.setState({ loading: true });
183
-      ConnectionManager.getInstance()
184
-        .connect(email, password)
185
-        .then(this.handleSuccess)
186
-        .catch(this.showErrorDialog)
187
-        .finally(() => {
188
-          this.setState({ loading: false });
189
-        });
190
-    }
72
+  const onSubmit = (email: string, password: string) => {
73
+    setLoading(true);
74
+    connectToAmicale(email, password)
75
+      .then(handleSuccess)
76
+      .catch(setCurrentError)
77
+      .finally(() => setLoading(false));
191
   };
78
   };
192
 
79
 
193
-  /**
194
-   * Gets the form input
195
-   *
196
-   * @returns {*}
197
-   */
198
-  getFormInput() {
199
-    const { email, password } = this.state;
200
-    return (
201
-      <View>
202
-        <TextInput
203
-          label={i18n.t('screens.login.email')}
204
-          mode="outlined"
205
-          value={email}
206
-          onChangeText={this.onEmailChange}
207
-          onBlur={this.validateEmail}
208
-          onSubmitEditing={this.onEmailSubmit}
209
-          error={this.shouldShowEmailError()}
210
-          textContentType="emailAddress"
211
-          autoCapitalize="none"
212
-          autoCompleteType="email"
213
-          autoCorrect={false}
214
-          keyboardType="email-address"
215
-          returnKeyType="next"
216
-          secureTextEntry={false}
217
-        />
218
-        <HelperText type="error" visible={this.shouldShowEmailError()}>
219
-          {i18n.t('screens.login.emailError')}
220
-        </HelperText>
221
-        <TextInput
222
-          ref={this.passwordInputRef}
223
-          label={i18n.t('screens.login.password')}
224
-          mode="outlined"
225
-          value={password}
226
-          onChangeText={this.onPasswordChange}
227
-          onBlur={this.validatePassword}
228
-          onSubmitEditing={this.onSubmit}
229
-          error={this.shouldShowPasswordError()}
230
-          textContentType="password"
231
-          autoCapitalize="none"
232
-          autoCompleteType="password"
233
-          autoCorrect={false}
234
-          keyboardType="default"
235
-          returnKeyType="done"
236
-          secureTextEntry
237
-        />
238
-        <HelperText type="error" visible={this.shouldShowPasswordError()}>
239
-          {i18n.t('screens.login.passwordError')}
240
-        </HelperText>
241
-      </View>
242
-    );
243
-  }
80
+  const hideMascotDialog = () => setMascotDialogVisible(true);
244
 
81
 
245
-  /**
246
-   * Gets the card containing the input form
247
-   * @returns {*}
248
-   */
249
-  getMainCard() {
250
-    const { props, state } = this;
251
-    return (
252
-      <View style={styles.card}>
253
-        <Card.Title
254
-          title={i18n.t('screens.login.title')}
255
-          titleStyle={styles.text}
256
-          subtitle={i18n.t('screens.login.subtitle')}
257
-          subtitleStyle={styles.text}
258
-          left={({ size }) => (
259
-            <Image
260
-              source={ICON_AMICALE}
261
-              style={{
262
-                width: size,
263
-                height: size,
264
-              }}
265
-            />
266
-          )}
267
-        />
268
-        <Card.Content>
269
-          {this.getFormInput()}
270
-          <Card.Actions style={styles.buttonContainer}>
271
-            <Button
272
-              icon="lock-question"
273
-              mode="contained"
274
-              onPress={this.onResetPasswordClick}
275
-              color={props.theme.colors.warning}
276
-              style={styles.lockButton}
277
-            >
278
-              {i18n.t('screens.login.resetPassword')}
279
-            </Button>
280
-            <Button
281
-              icon="send"
282
-              mode="contained"
283
-              disabled={!this.shouldEnableLogin()}
284
-              loading={state.loading}
285
-              onPress={this.onSubmit}
286
-              style={styles.sendButton}
287
-            >
288
-              {i18n.t('screens.login.title')}
289
-            </Button>
290
-          </Card.Actions>
291
-          <Card.Actions>
292
-            <Button
293
-              icon="help-circle"
294
-              mode="contained"
295
-              onPress={this.showMascotDialog}
296
-              style={GENERAL_STYLES.centerHorizontal}
297
-            >
298
-              {i18n.t('screens.login.mascotDialog.title')}
299
-            </Button>
300
-          </Card.Actions>
301
-        </Card.Content>
302
-      </View>
303
-    );
304
-  }
82
+  const showMascotDialog = () => setMascotDialogVisible(false);
305
 
83
 
306
-  /**
307
-   * The user has unfocused the input, his email is ready to be validated
308
-   */
309
-  validateEmail = () => {
310
-    this.setState({ isEmailValidated: true });
311
-  };
312
-
313
-  /**
314
-   * The user has unfocused the input, his password is ready to be validated
315
-   */
316
-  validatePassword = () => {
317
-    this.setState({ isPasswordValidated: true });
318
-  };
319
-
320
-  hideMascotDialog = () => {
321
-    this.setState({ mascotDialogVisible: false });
322
-  };
323
-
324
-  showMascotDialog = () => {
325
-    this.setState({ mascotDialogVisible: true });
326
-  };
327
-
328
-  /**
329
-   * Shows an error dialog with the corresponding login error
330
-   *
331
-   * @param error The error given by the login request
332
-   */
333
-  showErrorDialog = (error: ApiRejectType) => {
334
-    console.log(error);
335
-
336
-    this.setState({
337
-      dialogVisible: true,
338
-      dialogError: error,
339
-    });
340
-  };
341
-
342
-  hideErrorDialog = () => {
343
-    this.setState({ dialogVisible: false });
344
-  };
84
+  const hideErrorDialog = () =>
85
+    setCurrentError({ status: REQUEST_STATUS.SUCCESS });
345
 
86
 
346
   /**
87
   /**
347
    * Navigates to the screen specified in navigation parameters or simply go back tha stack.
88
    * Navigates to the screen specified in navigation parameters or simply go back tha stack.
348
    * Saves in user preferences to not show the login banner again.
89
    * Saves in user preferences to not show the login banner again.
349
    */
90
    */
350
-  handleSuccess = () => {
351
-    const { navigation } = this.props;
91
+  const handleSuccess = () => {
352
     // Do not show the home login banner again
92
     // Do not show the home login banner again
353
-    // TODO
354
-    // AsyncStorageManager.set(
355
-    //   AsyncStorageManager.PREFERENCES.homeShowMascot.key,
356
-    //   false
357
-    // );
358
-    if (this.nextScreen == null) {
93
+    if (homeMascot.shouldShow) {
94
+      homeMascot.setShouldShow(false);
95
+    }
96
+    if (!nextScreen) {
359
       navigation.goBack();
97
       navigation.goBack();
360
     } else {
98
     } else {
361
-      navigation.replace(this.nextScreen);
99
+      navigation.replace(nextScreen);
362
     }
100
     }
363
   };
101
   };
364
 
102
 
365
-  /**
366
-   * Saves the screen to navigate to after a successful login if one was provided in navigation parameters
367
-   */
368
-  handleNavigationParams() {
369
-    this.nextScreen = this.props.route.params?.nextScreen;
370
-  }
371
-
372
-  /**
373
-   * Checks if the entered email is valid (matches the regex)
374
-   *
375
-   * @returns {boolean}
376
-   */
377
-  isEmailValid(): boolean {
378
-    const { email } = this.state;
379
-    return emailRegex.test(email);
380
-  }
381
-
382
-  /**
383
-   * Checks if we should tell the user his email is invalid.
384
-   * We should only show this if his email is invalid and has been checked when un-focusing the input
385
-   *
386
-   * @returns {boolean|boolean}
387
-   */
388
-  shouldShowEmailError(): boolean {
389
-    const { isEmailValidated } = this.state;
390
-    return isEmailValidated && !this.isEmailValid();
391
-  }
392
-
393
-  /**
394
-   * Checks if the user has entered a password
395
-   *
396
-   * @returns {boolean}
397
-   */
398
-  isPasswordValid(): boolean {
399
-    const { password } = this.state;
400
-    return password !== '';
401
-  }
402
-
403
-  /**
404
-   * Checks if we should tell the user his password is invalid.
405
-   * We should only show this if his password is invalid and has been checked when un-focusing the input
406
-   *
407
-   * @returns {boolean|boolean}
408
-   */
409
-  shouldShowPasswordError(): boolean {
410
-    const { isPasswordValidated } = this.state;
411
-    return isPasswordValidated && !this.isPasswordValid();
412
-  }
413
-
414
-  /**
415
-   * If the email and password are valid, and we are not loading a request, then the login button can be enabled
416
-   *
417
-   * @returns {boolean}
418
-   */
419
-  shouldEnableLogin(): boolean {
420
-    const { loading } = this.state;
421
-    return this.isEmailValid() && this.isPasswordValid() && !loading;
422
-  }
423
-
424
-  render() {
425
-    const { mascotDialogVisible, dialogVisible, dialogError } = this.state;
426
-    return (
427
-      <LinearGradient
103
+  return (
104
+    <LinearGradient
105
+      style={GENERAL_STYLES.flex}
106
+      colors={['#9e0d18', '#530209']}
107
+      start={{ x: 0, y: 0.1 }}
108
+      end={{ x: 0.1, y: 1 }}
109
+    >
110
+      <KeyboardAvoidingView
111
+        behavior={'height'}
112
+        contentContainerStyle={GENERAL_STYLES.flex}
428
         style={GENERAL_STYLES.flex}
113
         style={GENERAL_STYLES.flex}
429
-        colors={['#9e0d18', '#530209']}
430
-        start={{ x: 0, y: 0.1 }}
431
-        end={{ x: 0.1, y: 1 }}
114
+        enabled={true}
115
+        keyboardVerticalOffset={100}
432
       >
116
       >
433
-        <KeyboardAvoidingView
434
-          behavior={'height'}
435
-          contentContainerStyle={GENERAL_STYLES.flex}
436
-          style={GENERAL_STYLES.flex}
437
-          enabled={true}
438
-          keyboardVerticalOffset={100}
439
-        >
440
-          <CollapsibleScrollView headerColors={'transparent'}>
441
-            <View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
442
-            <MascotPopup
443
-              visible={mascotDialogVisible}
444
-              title={i18n.t('screens.login.mascotDialog.title')}
445
-              message={i18n.t('screens.login.mascotDialog.message')}
446
-              icon={'help'}
447
-              buttons={{
448
-                cancel: {
449
-                  message: i18n.t('screens.login.mascotDialog.button'),
450
-                  icon: 'check',
451
-                  onPress: this.hideMascotDialog,
452
-                },
453
-              }}
454
-              emotion={MASCOT_STYLE.NORMAL}
455
-            />
456
-            <ErrorDialog
457
-              visible={dialogVisible}
458
-              onDismiss={this.hideErrorDialog}
459
-              status={dialogError.status}
460
-              code={dialogError.code}
117
+        <CollapsibleScrollView headerColors={'transparent'}>
118
+          <View style={GENERAL_STYLES.flex}>
119
+            <LoginForm
120
+              loading={loading}
121
+              onSubmit={onSubmit}
122
+              onResetPasswordPress={onResetPasswordClick}
123
+              onHelpPress={showMascotDialog}
461
             />
124
             />
462
-          </CollapsibleScrollView>
463
-        </KeyboardAvoidingView>
464
-      </LinearGradient>
465
-    );
466
-  }
125
+          </View>
126
+          <MascotPopup
127
+            visible={mascotDialogVisible}
128
+            title={i18n.t('screens.login.mascotDialog.title')}
129
+            message={i18n.t('screens.login.mascotDialog.message')}
130
+            icon={'help'}
131
+            buttons={{
132
+              cancel: {
133
+                message: i18n.t('screens.login.mascotDialog.button'),
134
+                icon: 'check',
135
+                onPress: hideMascotDialog,
136
+              },
137
+            }}
138
+            emotion={MASCOT_STYLE.NORMAL}
139
+          />
140
+          <ErrorDialog
141
+            visible={currentError.status !== REQUEST_STATUS.SUCCESS}
142
+            onDismiss={hideErrorDialog}
143
+            status={currentError.status}
144
+            code={currentError.code}
145
+          />
146
+        </CollapsibleScrollView>
147
+      </KeyboardAvoidingView>
148
+    </LinearGradient>
149
+  );
467
 }
150
 }
468
 
151
 
469
-export default withTheme(LoginScreen);
152
+export default LoginScreen;

+ 72
- 443
src/screens/Amicale/ProfileScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
21
-import { FlatList, StyleSheet, View } from 'react-native';
22
-import {
23
-  Avatar,
24
-  Button,
25
-  Card,
26
-  Divider,
27
-  List,
28
-  Paragraph,
29
-  withTheme,
30
-} from 'react-native-paper';
31
-import i18n from 'i18n-js';
32
-import { StackNavigationProp } from '@react-navigation/stack';
20
+import React, { useLayoutEffect, useState } from 'react';
21
+import { View } from 'react-native';
33
 import LogoutDialog from '../../components/Amicale/LogoutDialog';
22
 import LogoutDialog from '../../components/Amicale/LogoutDialog';
34
 import MaterialHeaderButtons, {
23
 import MaterialHeaderButtons, {
35
   Item,
24
   Item,
36
 } from '../../components/Overrides/CustomHeaderButton';
25
 } from '../../components/Overrides/CustomHeaderButton';
37
-import CardList from '../../components/Lists/CardList/CardList';
38
-import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot';
39
 import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
26
 import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
40
 import GENERAL_STYLES from '../../constants/Styles';
27
 import GENERAL_STYLES from '../../constants/Styles';
41
-import Urls from '../../constants/Urls';
42
 import RequestScreen from '../../components/Screens/RequestScreen';
28
 import RequestScreen from '../../components/Screens/RequestScreen';
43
-import ConnectionManager from '../../managers/ConnectionManager';
44
-import {
45
-  getAmicaleServices,
46
-  ServiceItemType,
47
-  SERVICES_KEY,
48
-} from '../../utils/Services';
49
-
50
-type PropsType = {
51
-  navigation: StackNavigationProp<any>;
52
-  theme: ReactNativePaper.Theme;
53
-};
54
-
55
-type StateType = {
56
-  dialogVisible: boolean;
57
-};
58
-
59
-type ClubType = {
29
+import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
30
+import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
31
+import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
32
+import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
33
+import { useNavigation } from '@react-navigation/core';
34
+import { useAuthenticatedRequest } from '../../context/loginContext';
35
+
36
+export type ProfileClubType = {
60
   id: number;
37
   id: number;
61
   name: string;
38
   name: string;
62
   is_manager: boolean;
39
   is_manager: boolean;
63
 };
40
 };
64
 
41
 
65
-type ProfileDataType = {
42
+export type ProfileDataType = {
66
   first_name: string;
43
   first_name: string;
67
   last_name: string;
44
   last_name: string;
68
   email: string;
45
   email: string;
71
   branch: string;
48
   branch: string;
72
   link: string;
49
   link: string;
73
   validity: boolean;
50
   validity: boolean;
74
-  clubs: Array<ClubType>;
51
+  clubs: Array<ProfileClubType>;
75
 };
52
 };
76
 
53
 
77
-const styles = StyleSheet.create({
78
-  card: {
79
-    margin: 10,
80
-  },
81
-  icon: {
82
-    backgroundColor: 'transparent',
83
-  },
84
-  editButton: {
85
-    marginLeft: 'auto',
86
-  },
87
-  mascot: {
88
-    width: 60,
89
-  },
90
-  title: {
91
-    marginLeft: 10,
92
-  },
93
-});
94
-
95
-class ProfileScreen extends React.Component<PropsType, StateType> {
96
-  data: ProfileDataType | undefined;
97
-
98
-  flatListData: Array<{ id: string }>;
99
-
100
-  amicaleDataset: Array<ServiceItemType>;
101
-
102
-  constructor(props: PropsType) {
103
-    super(props);
104
-    this.data = undefined;
105
-    this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
106
-    this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [
107
-      SERVICES_KEY.PROFILE,
108
-    ]);
109
-    this.state = {
110
-      dialogVisible: false,
111
-    };
112
-  }
113
-
114
-  componentDidMount() {
115
-    const { navigation } = this.props;
54
+function ProfileScreen() {
55
+  const navigation = useNavigation();
56
+  const [dialogVisible, setDialogVisible] = useState(false);
57
+  const request = useAuthenticatedRequest<ProfileDataType>('user/profile');
58
+
59
+  useLayoutEffect(() => {
60
+    const getHeaderButton = () => (
61
+      <MaterialHeaderButtons>
62
+        <Item
63
+          title={'logout'}
64
+          iconName={'logout'}
65
+          onPress={showDisconnectDialog}
66
+        />
67
+      </MaterialHeaderButtons>
68
+    );
116
     navigation.setOptions({
69
     navigation.setOptions({
117
-      headerRight: this.getHeaderButton,
70
+      headerRight: getHeaderButton,
118
     });
71
     });
119
-  }
120
-
121
-  /**
122
-   * Gets the logout header button
123
-   *
124
-   * @returns {*}
125
-   */
126
-  getHeaderButton = () => (
127
-    <MaterialHeaderButtons>
128
-      <Item
129
-        title="logout"
130
-        iconName="logout"
131
-        onPress={this.showDisconnectDialog}
132
-      />
133
-    </MaterialHeaderButtons>
134
-  );
72
+  }, [navigation]);
135
 
73
 
136
-  /**
137
-   * Gets the main screen component with the fetched data
138
-   *
139
-   * @param data The data fetched from the server
140
-   * @returns {*}
141
-   */
142
-  getScreen = (data: ProfileDataType | undefined) => {
143
-    const { dialogVisible } = this.state;
74
+  const getScreen = (data: ProfileDataType | undefined) => {
144
     if (data) {
75
     if (data) {
145
-      this.data = data;
76
+      const flatListData: Array<{
77
+        id: string;
78
+        render: () => React.ReactElement;
79
+      }> = [];
80
+      for (let i = 0; i < 4; i++) {
81
+        switch (i) {
82
+          case 0:
83
+            flatListData.push({
84
+              id: i.toString(),
85
+              render: () => <ProfileWelcomeCard firstname={data?.first_name} />,
86
+            });
87
+            break;
88
+          case 1:
89
+            flatListData.push({
90
+              id: i.toString(),
91
+              render: () => <ProfilePersonalCard profile={data} />,
92
+            });
93
+            break;
94
+          case 2:
95
+            flatListData.push({
96
+              id: i.toString(),
97
+              render: () => <ProfileClubCard clubs={data?.clubs} />,
98
+            });
99
+            break;
100
+          default:
101
+            flatListData.push({
102
+              id: i.toString(),
103
+              render: () => <ProfileMembershipCard valid={data?.validity} />,
104
+            });
105
+        }
106
+      }
146
       return (
107
       return (
147
         <View style={GENERAL_STYLES.flex}>
108
         <View style={GENERAL_STYLES.flex}>
148
-          <CollapsibleFlatList
149
-            renderItem={this.getRenderItem}
150
-            data={this.flatListData}
151
-          />
109
+          <CollapsibleFlatList renderItem={getRenderItem} data={flatListData} />
152
           <LogoutDialog
110
           <LogoutDialog
153
             visible={dialogVisible}
111
             visible={dialogVisible}
154
-            onDismiss={this.hideDisconnectDialog}
112
+            onDismiss={hideDisconnectDialog}
155
           />
113
           />
156
         </View>
114
         </View>
157
       );
115
       );
160
     }
118
     }
161
   };
119
   };
162
 
120
 
163
-  getRenderItem = ({ item }: { item: { id: string } }) => {
164
-    switch (item.id) {
165
-      case '0':
166
-        return this.getWelcomeCard();
167
-      case '1':
168
-        return this.getPersonalCard();
169
-      case '2':
170
-        return this.getClubCard();
171
-      default:
172
-        return this.getMembershipCar();
173
-    }
174
-  };
175
-
176
-  /**
177
-   * Gets the list of services available with the Amicale account
178
-   *
179
-   * @returns {*}
180
-   */
181
-  getServicesList() {
182
-    return <CardList dataset={this.amicaleDataset} isHorizontal />;
183
-  }
184
-
185
-  /**
186
-   * Gets a card welcoming the user to his account
187
-   *
188
-   * @returns {*}
189
-   */
190
-  getWelcomeCard() {
191
-    const { navigation } = this.props;
192
-    return (
193
-      <Card style={styles.card}>
194
-        <Card.Title
195
-          title={i18n.t('screens.profile.welcomeTitle', {
196
-            name: this.data?.first_name,
197
-          })}
198
-          left={() => (
199
-            <Mascot
200
-              style={styles.mascot}
201
-              emotion={MASCOT_STYLE.COOL}
202
-              animated
203
-              entryAnimation={{
204
-                animation: 'bounceIn',
205
-                duration: 1000,
206
-              }}
207
-            />
208
-          )}
209
-          titleStyle={styles.title}
210
-        />
211
-        <Card.Content>
212
-          <Divider />
213
-          <Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
214
-          {this.getServicesList()}
215
-          <Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
216
-          <Divider />
217
-          <Card.Actions>
218
-            <Button
219
-              icon="bug"
220
-              mode="contained"
221
-              onPress={() => {
222
-                navigation.navigate('feedback');
223
-              }}
224
-              style={styles.editButton}
225
-            >
226
-              {i18n.t('screens.feedback.homeButtonTitle')}
227
-            </Button>
228
-          </Card.Actions>
229
-        </Card.Content>
230
-      </Card>
231
-    );
232
-  }
233
-
234
-  /**
235
-   * Gets the given field value.
236
-   * If the field does not have a value, returns a placeholder text
237
-   *
238
-   * @param field The field to get the value from
239
-   * @return {*}
240
-   */
241
-  static getFieldValue(field?: string): string {
242
-    return field ? field : i18n.t('screens.profile.noData');
243
-  }
244
-
245
-  /**
246
-   * Gets a list item showing personal information
247
-   *
248
-   * @param field The field to display
249
-   * @param icon The icon to use
250
-   * @return {*}
251
-   */
252
-  getPersonalListItem(field: string | undefined, icon: string) {
253
-    const { theme } = this.props;
254
-    const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
255
-    const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
256
-    return (
257
-      <List.Item
258
-        title={title}
259
-        description={subtitle}
260
-        left={(props) => (
261
-          <List.Icon
262
-            style={props.style}
263
-            icon={icon}
264
-            color={field != null ? props.color : theme.colors.textDisabled}
265
-          />
266
-        )}
267
-      />
268
-    );
269
-  }
270
-
271
-  /**
272
-   * Gets a card containing user personal information
273
-   *
274
-   * @return {*}
275
-   */
276
-  getPersonalCard() {
277
-    const { theme, navigation } = this.props;
278
-    return (
279
-      <Card style={styles.card}>
280
-        <Card.Title
281
-          title={`${this.data?.first_name} ${this.data?.last_name}`}
282
-          subtitle={this.data?.email}
283
-          left={(iconProps) => (
284
-            <Avatar.Icon
285
-              size={iconProps.size}
286
-              icon="account"
287
-              color={theme.colors.primary}
288
-              style={styles.icon}
289
-            />
290
-          )}
291
-        />
292
-        <Card.Content>
293
-          <Divider />
294
-          <List.Section>
295
-            <List.Subheader>
296
-              {i18n.t('screens.profile.personalInformation')}
297
-            </List.Subheader>
298
-            {this.getPersonalListItem(this.data?.birthday, 'cake-variant')}
299
-            {this.getPersonalListItem(this.data?.phone, 'phone')}
300
-            {this.getPersonalListItem(this.data?.email, 'email')}
301
-            {this.getPersonalListItem(this.data?.branch, 'school')}
302
-          </List.Section>
303
-          <Divider />
304
-          <Card.Actions>
305
-            <Button
306
-              icon="account-edit"
307
-              mode="contained"
308
-              onPress={() => {
309
-                navigation.navigate('website', {
310
-                  host: Urls.websites.amicale,
311
-                  path: this.data?.link,
312
-                  title: i18n.t('screens.websites.amicale'),
313
-                });
314
-              }}
315
-              style={styles.editButton}
316
-            >
317
-              {i18n.t('screens.profile.editInformation')}
318
-            </Button>
319
-          </Card.Actions>
320
-        </Card.Content>
321
-      </Card>
322
-    );
323
-  }
324
-
325
-  /**
326
-   * Gets a cars containing clubs the user is part of
327
-   *
328
-   * @return {*}
329
-   */
330
-  getClubCard() {
331
-    const { theme } = this.props;
332
-    return (
333
-      <Card style={styles.card}>
334
-        <Card.Title
335
-          title={i18n.t('screens.profile.clubs')}
336
-          subtitle={i18n.t('screens.profile.clubsSubtitle')}
337
-          left={(iconProps) => (
338
-            <Avatar.Icon
339
-              size={iconProps.size}
340
-              icon="account-group"
341
-              color={theme.colors.primary}
342
-              style={styles.icon}
343
-            />
344
-          )}
345
-        />
346
-        <Card.Content>
347
-          <Divider />
348
-          {this.getClubList(this.data?.clubs)}
349
-        </Card.Content>
350
-      </Card>
351
-    );
352
-  }
353
-
354
-  /**
355
-   * Gets a card showing if the user has payed his membership
356
-   *
357
-   * @return {*}
358
-   */
359
-  getMembershipCar() {
360
-    const { theme } = this.props;
361
-    return (
362
-      <Card style={styles.card}>
363
-        <Card.Title
364
-          title={i18n.t('screens.profile.membership')}
365
-          subtitle={i18n.t('screens.profile.membershipSubtitle')}
366
-          left={(iconProps) => (
367
-            <Avatar.Icon
368
-              size={iconProps.size}
369
-              icon="credit-card"
370
-              color={theme.colors.primary}
371
-              style={styles.icon}
372
-            />
373
-          )}
374
-        />
375
-        <Card.Content>
376
-          <List.Section>
377
-            {this.getMembershipItem(this.data?.validity === true)}
378
-          </List.Section>
379
-        </Card.Content>
380
-      </Card>
381
-    );
382
-  }
383
-
384
-  /**
385
-   * Gets the item showing if the user has payed his membership
386
-   *
387
-   * @return {*}
388
-   */
389
-  getMembershipItem(state: boolean) {
390
-    const { theme } = this.props;
391
-    return (
392
-      <List.Item
393
-        title={
394
-          state
395
-            ? i18n.t('screens.profile.membershipPayed')
396
-            : i18n.t('screens.profile.membershipNotPayed')
397
-        }
398
-        left={(props) => (
399
-          <List.Icon
400
-            style={props.style}
401
-            color={state ? theme.colors.success : theme.colors.danger}
402
-            icon={state ? 'check' : 'close'}
403
-          />
404
-        )}
405
-      />
406
-    );
407
-  }
408
-
409
-  /**
410
-   * Gets a list item for the club list
411
-   *
412
-   * @param item The club to render
413
-   * @return {*}
414
-   */
415
-  getClubListItem = ({ item }: { item: ClubType }) => {
416
-    const { theme } = this.props;
417
-    const onPress = () => {
418
-      this.openClubDetailsScreen(item.id);
419
-    };
420
-    let description = i18n.t('screens.profile.isMember');
421
-    let icon = (props: {
422
-      color: string;
423
-      style: {
424
-        marginLeft: number;
425
-        marginRight: number;
426
-        marginVertical?: number;
427
-      };
428
-    }) => (
429
-      <List.Icon color={props.color} style={props.style} icon="chevron-right" />
430
-    );
431
-    if (item.is_manager) {
432
-      description = i18n.t('screens.profile.isManager');
433
-      icon = (props) => (
434
-        <List.Icon
435
-          style={props.style}
436
-          icon="star"
437
-          color={theme.colors.primary}
438
-        />
439
-      );
440
-    }
441
-    return (
442
-      <List.Item
443
-        title={item.name}
444
-        description={description}
445
-        left={icon}
446
-        onPress={onPress}
447
-      />
448
-    );
449
-  };
450
-
451
-  /**
452
-   * Renders the list of clubs the user is part of
453
-   *
454
-   * @param list The club list
455
-   * @return {*}
456
-   */
457
-  getClubList(list: Array<ClubType> | undefined) {
458
-    if (!list) {
459
-      return null;
460
-    }
461
-
462
-    list.sort(this.sortClubList);
463
-    return (
464
-      <FlatList
465
-        renderItem={this.getClubListItem}
466
-        keyExtractor={this.clubKeyExtractor}
467
-        data={list}
468
-      />
469
-    );
470
-  }
471
-
472
-  clubKeyExtractor = (item: ClubType): string => item.name;
473
-
474
-  sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
121
+  const getRenderItem = ({
122
+    item,
123
+  }: {
124
+    item: { id: string; render: () => React.ReactElement };
125
+  }) => item.render();
475
 
126
 
476
-  showDisconnectDialog = () => {
477
-    this.setState({ dialogVisible: true });
478
-  };
127
+  const showDisconnectDialog = () => setDialogVisible(true);
479
 
128
 
480
-  hideDisconnectDialog = () => {
481
-    this.setState({ dialogVisible: false });
482
-  };
129
+  const hideDisconnectDialog = () => setDialogVisible(false);
483
 
130
 
484
-  /**
485
-   * Opens the club details screen for the club of given ID
486
-   * @param id The club's id to open
487
-   */
488
-  openClubDetailsScreen(id: number) {
489
-    const { navigation } = this.props;
490
-    navigation.navigate('club-information', { clubId: id });
491
-  }
492
-
493
-  render() {
494
-    return (
495
-      <RequestScreen<ProfileDataType>
496
-        request={() =>
497
-          ConnectionManager.getInstance().authenticatedRequest('user/profile')
498
-        }
499
-        render={this.getScreen}
500
-      />
501
-    );
502
-  }
131
+  return <RequestScreen request={request} render={getScreen} />;
503
 }
132
 }
504
 
133
 
505
-export default withTheme(ProfileScreen);
134
+export default ProfileScreen;

+ 154
- 176
src/screens/Amicale/VoteScreen.tsx View File

17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
17
  * along with Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
18
  */
18
  */
19
 
19
 
20
-import * as React from 'react';
20
+import React, { useRef, useState } from 'react';
21
 import { StyleSheet, View } from 'react-native';
21
 import { StyleSheet, View } from 'react-native';
22
 import i18n from 'i18n-js';
22
 import i18n from 'i18n-js';
23
 import { Button } from 'react-native-paper';
23
 import { Button } from 'react-native-paper';
30
 import MascotPopup from '../../components/Mascot/MascotPopup';
30
 import MascotPopup from '../../components/Mascot/MascotPopup';
31
 import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
31
 import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
32
 import GENERAL_STYLES from '../../constants/Styles';
32
 import GENERAL_STYLES from '../../constants/Styles';
33
-import ConnectionManager from '../../managers/ConnectionManager';
34
 import WebSectionList, {
33
 import WebSectionList, {
35
   SectionListDataType,
34
   SectionListDataType,
36
 } from '../../components/Screens/WebSectionList';
35
 } from '../../components/Screens/WebSectionList';
36
+import { useAuthenticatedRequest } from '../../context/loginContext';
37
 
37
 
38
 export type VoteTeamType = {
38
 export type VoteTeamType = {
39
   id: number;
39
   id: number;
65
   dates?: VoteDatesStringType;
65
   dates?: VoteDatesStringType;
66
 };
66
 };
67
 
67
 
68
+type FlatlistType = {
69
+  teams: Array<VoteTeamType>;
70
+  hasVoted: boolean;
71
+  datesString?: VoteDatesStringType;
72
+  dates?: VoteDatesObjectType;
73
+};
74
+
68
 // const FAKE_DATE = {
75
 // const FAKE_DATE = {
69
 //     "date_begin": "2020-08-19 15:50",
76
 //     "date_begin": "2020-08-19 15:50",
70
 //     "date_end": "2020-08-19 15:50",
77
 //     "date_end": "2020-08-19 15:50",
113
 //     ],
120
 //     ],
114
 // };
121
 // };
115
 
122
 
116
-type PropsType = {};
117
-
118
-type StateType = {
119
-  hasVoted: boolean;
120
-  mascotDialogVisible: boolean | undefined;
121
-};
122
-
123
 const styles = StyleSheet.create({
123
 const styles = StyleSheet.create({
124
   button: {
124
   button: {
125
     marginLeft: 'auto',
125
     marginLeft: 'auto',
131
 /**
131
 /**
132
  * Screen displaying vote information and controls
132
  * Screen displaying vote information and controls
133
  */
133
  */
134
-export default class VoteScreen extends React.Component<PropsType, StateType> {
135
-  teams: Array<VoteTeamType>;
136
-
137
-  hasVoted: boolean;
138
-
139
-  datesString: undefined | VoteDatesStringType;
140
-
141
-  dates: undefined | VoteDatesObjectType;
142
-
143
-  today: Date;
144
-
145
-  mainFlatListData: SectionListDataType<{ key: string }>;
146
-
147
-  refreshData: () => void;
148
-
149
-  constructor(props: PropsType) {
150
-    super(props);
151
-    this.teams = [];
152
-    this.datesString = undefined;
153
-    this.dates = undefined;
154
-    this.state = {
155
-      hasVoted: false,
156
-      mascotDialogVisible: undefined,
157
-    };
158
-    this.hasVoted = false;
159
-    this.today = new Date();
160
-    this.refreshData = () => undefined;
161
-    this.mainFlatListData = [
162
-      { title: '', data: [{ key: 'main' }, { key: 'info' }] },
163
-    ];
164
-  }
165
-
134
+export default function VoteScreen() {
135
+  const [hasVoted, setHasVoted] = useState(false);
136
+  const [mascotDialogVisible, setMascotDialogVisible] = useState(false);
137
+
138
+  const datesRequest = useAuthenticatedRequest<VoteDatesStringType>(
139
+    'elections/dates'
140
+  );
141
+  const teamsRequest = useAuthenticatedRequest<TeamResponseType>(
142
+    'elections/teams'
143
+  );
144
+
145
+  const today = new Date();
146
+  const refresh = useRef<() => void | undefined>();
166
   /**
147
   /**
167
    * Gets the string representation of the given date.
148
    * Gets the string representation of the given date.
168
    *
149
    *
173
    * @param dateString The string representation of the wanted date
154
    * @param dateString The string representation of the wanted date
174
    * @returns {string}
155
    * @returns {string}
175
    */
156
    */
176
-  getDateString(date: Date, dateString: string): string {
177
-    if (this.today.getDate() === date.getDate()) {
157
+  const getDateString = (date: Date, dateString: string) => {
158
+    if (today.getDate() === date.getDate()) {
178
       const str = getTimeOnlyString(dateString);
159
       const str = getTimeOnlyString(dateString);
179
       return str != null ? str : '';
160
       return str != null ? str : '';
180
     }
161
     }
181
     return dateString;
162
     return dateString;
182
-  }
163
+  };
183
 
164
 
184
-  getMainRenderItem = ({ item }: { item: { key: string } }) => {
165
+  const getMainRenderItem = ({
166
+    item,
167
+  }: {
168
+    item: { key: string; data?: FlatlistType };
169
+  }) => {
185
     if (item.key === 'info') {
170
     if (item.key === 'info') {
186
       return (
171
       return (
187
         <View>
172
         <View>
188
           <Button
173
           <Button
189
             mode="contained"
174
             mode="contained"
190
             icon="help-circle"
175
             icon="help-circle"
191
-            onPress={this.showMascotDialog}
176
+            onPress={showMascotDialog}
192
             style={styles.button}
177
             style={styles.button}
193
           >
178
           >
194
             {i18n.t('screens.vote.mascotDialog.title')}
179
             {i18n.t('screens.vote.mascotDialog.title')}
196
         </View>
181
         </View>
197
       );
182
       );
198
     }
183
     }
199
-    return this.getContent();
184
+    if (item.data) {
185
+      return getContent(item.data);
186
+    } else {
187
+      return <View />;
188
+    }
200
   };
189
   };
201
 
190
 
202
-  createDataset = (
191
+  const createDataset = (
203
     data: ResponseType | undefined,
192
     data: ResponseType | undefined,
204
     _loading: boolean,
193
     _loading: boolean,
205
     _lastRefreshDate: Date | undefined,
194
     _lastRefreshDate: Date | undefined,
207
   ) => {
196
   ) => {
208
     // data[0] = FAKE_TEAMS2;
197
     // data[0] = FAKE_TEAMS2;
209
     // data[1] = FAKE_DATE;
198
     // data[1] = FAKE_DATE;
210
-    this.refreshData = refreshData;
199
+
200
+    const mainFlatListData: SectionListDataType<{
201
+      key: string;
202
+      data?: FlatlistType;
203
+    }> = [
204
+      {
205
+        title: '',
206
+        data: [{ key: 'main' }, { key: 'info' }],
207
+      },
208
+    ];
209
+    refresh.current = refreshData;
211
     if (data) {
210
     if (data) {
212
       const { teams, dates } = data;
211
       const { teams, dates } = data;
213
-
214
-      if (dates && dates.date_begin == null) {
215
-        this.datesString = undefined;
216
-      } else {
217
-        this.datesString = dates;
212
+      const flatlistData: FlatlistType = {
213
+        teams: [],
214
+        hasVoted: false,
215
+      };
216
+      if (dates && dates.date_begin != null) {
217
+        flatlistData.datesString = dates;
218
       }
218
       }
219
-
220
       if (teams) {
219
       if (teams) {
221
-        this.teams = teams.teams;
222
-        this.hasVoted = teams.has_voted;
220
+        flatlistData.teams = teams.teams;
221
+        flatlistData.hasVoted = teams.has_voted;
223
       }
222
       }
224
-
225
-      this.generateDateObject();
223
+      flatlistData.dates = generateDateObject(flatlistData.datesString);
226
     }
224
     }
227
-    return this.mainFlatListData;
225
+    return mainFlatListData;
228
   };
226
   };
229
 
227
 
230
-  getContent() {
231
-    const { state } = this;
232
-    if (!this.isVoteStarted()) {
233
-      return this.getTeaseVoteCard();
228
+  const getContent = (data: FlatlistType) => {
229
+    const { dates } = data;
230
+    if (!isVoteStarted(dates)) {
231
+      return getTeaseVoteCard(data);
234
     }
232
     }
235
-    if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
236
-      return this.getVoteCard();
233
+    if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
234
+      return getVoteCard(data);
237
     }
235
     }
238
-    if (!this.isResultStarted()) {
239
-      return this.getWaitVoteCard();
236
+    if (!isResultStarted(dates)) {
237
+      return getWaitVoteCard(data);
240
     }
238
     }
241
-    if (this.isResultRunning()) {
242
-      return this.getVoteResultCard();
239
+    if (isResultRunning(dates)) {
240
+      return getVoteResultCard(data);
243
     }
241
     }
244
     return <VoteNotAvailable />;
242
     return <VoteNotAvailable />;
245
-  }
246
-
247
-  onVoteSuccess = (): void => this.setState({ hasVoted: true });
243
+  };
248
 
244
 
245
+  const onVoteSuccess = () => setHasVoted(true);
249
   /**
246
   /**
250
    * The user has not voted yet, and the votes are open
247
    * The user has not voted yet, and the votes are open
251
    */
248
    */
252
-  getVoteCard() {
249
+  const getVoteCard = (data: FlatlistType) => {
253
     return (
250
     return (
254
       <VoteSelect
251
       <VoteSelect
255
-        teams={this.teams}
256
-        onVoteSuccess={this.onVoteSuccess}
257
-        onVoteError={this.refreshData}
252
+        teams={data.teams}
253
+        onVoteSuccess={onVoteSuccess}
254
+        onVoteError={() => {
255
+          if (refresh.current) {
256
+            refresh.current();
257
+          }
258
+        }}
258
       />
259
       />
259
     );
260
     );
260
-  }
261
-
261
+  };
262
   /**
262
   /**
263
    * Votes have ended, results can be displayed
263
    * Votes have ended, results can be displayed
264
    */
264
    */
265
-  getVoteResultCard() {
266
-    if (this.dates != null && this.datesString != null) {
265
+  const getVoteResultCard = (data: FlatlistType) => {
266
+    if (data.dates != null && data.datesString != null) {
267
       return (
267
       return (
268
         <VoteResults
268
         <VoteResults
269
-          teams={this.teams}
270
-          dateEnd={this.getDateString(
271
-            this.dates.date_result_end,
272
-            this.datesString.date_result_end
269
+          teams={data.teams}
270
+          dateEnd={getDateString(
271
+            data.dates.date_result_end,
272
+            data.datesString.date_result_end
273
           )}
273
           )}
274
         />
274
         />
275
       );
275
       );
276
     }
276
     }
277
     return <VoteNotAvailable />;
277
     return <VoteNotAvailable />;
278
-  }
279
-
278
+  };
280
   /**
279
   /**
281
    * Vote will open shortly
280
    * Vote will open shortly
282
    */
281
    */
283
-  getTeaseVoteCard() {
284
-    if (this.dates != null && this.datesString != null) {
282
+  const getTeaseVoteCard = (data: FlatlistType) => {
283
+    if (data.dates != null && data.datesString != null) {
285
       return (
284
       return (
286
         <VoteTease
285
         <VoteTease
287
-          startDate={this.getDateString(
288
-            this.dates.date_begin,
289
-            this.datesString.date_begin
286
+          startDate={getDateString(
287
+            data.dates.date_begin,
288
+            data.datesString.date_begin
290
           )}
289
           )}
291
         />
290
         />
292
       );
291
       );
293
     }
292
     }
294
     return <VoteNotAvailable />;
293
     return <VoteNotAvailable />;
295
-  }
296
-
294
+  };
297
   /**
295
   /**
298
    * Votes have ended, or user has voted waiting for results
296
    * Votes have ended, or user has voted waiting for results
299
    */
297
    */
300
-  getWaitVoteCard() {
301
-    const { state } = this;
298
+  const getWaitVoteCard = (data: FlatlistType) => {
302
     let startDate = null;
299
     let startDate = null;
303
     if (
300
     if (
304
-      this.dates != null &&
305
-      this.datesString != null &&
306
-      this.dates.date_result_begin != null
301
+      data.dates != null &&
302
+      data.datesString != null &&
303
+      data.dates.date_result_begin != null
307
     ) {
304
     ) {
308
-      startDate = this.getDateString(
309
-        this.dates.date_result_begin,
310
-        this.datesString.date_result_begin
305
+      startDate = getDateString(
306
+        data.dates.date_result_begin,
307
+        data.datesString.date_result_begin
311
       );
308
       );
312
     }
309
     }
313
     return (
310
     return (
314
       <VoteWait
311
       <VoteWait
315
         startDate={startDate}
312
         startDate={startDate}
316
-        hasVoted={this.hasVoted || state.hasVoted}
317
-        justVoted={state.hasVoted}
318
-        isVoteRunning={this.isVoteRunning()}
313
+        hasVoted={data.hasVoted}
314
+        justVoted={hasVoted}
315
+        isVoteRunning={isVoteRunning()}
319
       />
316
       />
320
     );
317
     );
321
-  }
322
-
323
-  showMascotDialog = () => {
324
-    this.setState({ mascotDialogVisible: true });
325
   };
318
   };
326
 
319
 
327
-  hideMascotDialog = () => {
328
-    this.setState({ mascotDialogVisible: false });
329
-  };
320
+  const showMascotDialog = () => setMascotDialogVisible(true);
330
 
321
 
331
-  isVoteStarted(): boolean {
332
-    return this.dates != null && this.today > this.dates.date_begin;
333
-  }
322
+  const hideMascotDialog = () => setMascotDialogVisible(false);
334
 
323
 
335
-  isResultRunning(): boolean {
324
+  const isVoteStarted = (dates?: VoteDatesObjectType) => {
325
+    return dates != null && today > dates.date_begin;
326
+  };
327
+
328
+  const isResultRunning = (dates?: VoteDatesObjectType) => {
336
     return (
329
     return (
337
-      this.dates != null &&
338
-      this.today > this.dates.date_result_begin &&
339
-      this.today < this.dates.date_result_end
330
+      dates != null &&
331
+      today > dates.date_result_begin &&
332
+      today < dates.date_result_end
340
     );
333
     );
341
-  }
334
+  };
342
 
335
 
343
-  isResultStarted(): boolean {
344
-    return this.dates != null && this.today > this.dates.date_result_begin;
345
-  }
336
+  const isResultStarted = (dates?: VoteDatesObjectType) => {
337
+    return dates != null && today > dates.date_result_begin;
338
+  };
346
 
339
 
347
-  isVoteRunning(): boolean {
348
-    return (
349
-      this.dates != null &&
350
-      this.today > this.dates.date_begin &&
351
-      this.today < this.dates.date_end
352
-    );
353
-  }
340
+  const isVoteRunning = (dates?: VoteDatesObjectType) => {
341
+    return dates != null && today > dates.date_begin && today < dates.date_end;
342
+  };
354
 
343
 
355
   /**
344
   /**
356
    * Generates the objects containing string and Date representations of key vote dates
345
    * Generates the objects containing string and Date representations of key vote dates
357
    */
346
    */
358
-  generateDateObject() {
359
-    const strings = this.datesString;
360
-    if (strings != null) {
347
+  const generateDateObject = (
348
+    strings?: VoteDatesStringType
349
+  ): VoteDatesObjectType | undefined => {
350
+    if (strings) {
361
       const dateBegin = stringToDate(strings.date_begin);
351
       const dateBegin = stringToDate(strings.date_begin);
362
       const dateEnd = stringToDate(strings.date_end);
352
       const dateEnd = stringToDate(strings.date_end);
363
       const dateResultBegin = stringToDate(strings.date_result_begin);
353
       const dateResultBegin = stringToDate(strings.date_result_begin);
368
         dateResultBegin != null &&
358
         dateResultBegin != null &&
369
         dateResultEnd != null
359
         dateResultEnd != null
370
       ) {
360
       ) {
371
-        this.dates = {
361
+        return {
372
           date_begin: dateBegin,
362
           date_begin: dateBegin,
373
           date_end: dateEnd,
363
           date_end: dateEnd,
374
           date_result_begin: dateResultBegin,
364
           date_result_begin: dateResultBegin,
375
           date_result_end: dateResultEnd,
365
           date_result_end: dateResultEnd,
376
         };
366
         };
377
       } else {
367
       } else {
378
-        this.dates = undefined;
368
+        return undefined;
379
       }
369
       }
380
     } else {
370
     } else {
381
-      this.dates = undefined;
371
+      return undefined;
382
     }
372
     }
383
-  }
373
+  };
384
 
374
 
385
-  request = () => {
375
+  const request = () => {
386
     return new Promise((resolve: (data: ResponseType) => void) => {
376
     return new Promise((resolve: (data: ResponseType) => void) => {
387
-      ConnectionManager.getInstance()
388
-        .authenticatedRequest<VoteDatesStringType>('elections/dates')
377
+      datesRequest()
389
         .then((datesData) => {
378
         .then((datesData) => {
390
-          ConnectionManager.getInstance()
391
-            .authenticatedRequest<TeamResponseType>('elections/teams')
379
+          teamsRequest()
392
             .then((teamsData) => {
380
             .then((teamsData) => {
393
               resolve({
381
               resolve({
394
                 dates: datesData,
382
                 dates: datesData,
405
     });
393
     });
406
   };
394
   };
407
 
395
 
408
-  /**
409
-   * Renders the authenticated screen.
410
-   *
411
-   * Teams and dates are not mandatory to allow showing the information box even if api requests fail
412
-   *
413
-   * @returns {*}
414
-   */
415
-  render() {
416
-    const { state } = this;
417
-    return (
418
-      <View style={GENERAL_STYLES.flex}>
419
-        <WebSectionList
420
-          request={this.request}
421
-          createDataset={this.createDataset}
422
-          extraData={state.hasVoted.toString()}
423
-          renderItem={this.getMainRenderItem}
424
-        />
425
-        <MascotPopup
426
-          visible={state.mascotDialogVisible}
427
-          title={i18n.t('screens.vote.mascotDialog.title')}
428
-          message={i18n.t('screens.vote.mascotDialog.message')}
429
-          icon="vote"
430
-          buttons={{
431
-            cancel: {
432
-              message: i18n.t('screens.vote.mascotDialog.button'),
433
-              icon: 'check',
434
-              onPress: this.hideMascotDialog,
435
-            },
436
-          }}
437
-          emotion={MASCOT_STYLE.CUTE}
438
-        />
439
-      </View>
440
-    );
441
-  }
396
+  return (
397
+    <View style={GENERAL_STYLES.flex}>
398
+      <WebSectionList
399
+        request={request}
400
+        createDataset={createDataset}
401
+        extraData={hasVoted.toString()}
402
+        renderItem={getMainRenderItem}
403
+      />
404
+      <MascotPopup
405
+        visible={mascotDialogVisible}
406
+        title={i18n.t('screens.vote.mascotDialog.title')}
407
+        message={i18n.t('screens.vote.mascotDialog.message')}
408
+        icon="vote"
409
+        buttons={{
410
+          cancel: {
411
+            message: i18n.t('screens.vote.mascotDialog.button'),
412
+            icon: 'check',
413
+            onPress: hideMascotDialog,
414
+          },
415
+        }}
416
+        emotion={MASCOT_STYLE.CUTE}
417
+      />
418
+    </View>
419
+  );
442
 }
420
 }

+ 2
- 9
src/screens/Home/HomeScreen.tsx View File

46
   Item,
46
   Item,
47
 } from '../../components/Overrides/CustomHeaderButton';
47
 } from '../../components/Overrides/CustomHeaderButton';
48
 import AnimatedFAB from '../../components/Animations/AnimatedFAB';
48
 import AnimatedFAB from '../../components/Animations/AnimatedFAB';
49
-import ConnectionManager from '../../managers/ConnectionManager';
50
 import LogoutDialog from '../../components/Amicale/LogoutDialog';
49
 import LogoutDialog from '../../components/Amicale/LogoutDialog';
51
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
50
 import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
52
 import MascotPopup from '../../components/Mascot/MascotPopup';
51
 import MascotPopup from '../../components/Mascot/MascotPopup';
59
 import { ServiceItemType } from '../../utils/Services';
58
 import { ServiceItemType } from '../../utils/Services';
60
 import { useCurrentDashboard } from '../../context/preferencesContext';
59
 import { useCurrentDashboard } from '../../context/preferencesContext';
61
 import { MainRoutes } from '../../navigation/MainNavigator';
60
 import { MainRoutes } from '../../navigation/MainNavigator';
61
+import { useLoginState } from '../../context/loginContext';
62
 
62
 
63
 const FEED_ITEM_HEIGHT = 500;
63
 const FEED_ITEM_HEIGHT = 500;
64
 
64
 
146
   const [dialogVisible, setDialogVisible] = useState(false);
146
   const [dialogVisible, setDialogVisible] = useState(false);
147
   const fabRef = useRef<AnimatedFAB>(null);
147
   const fabRef = useRef<AnimatedFAB>(null);
148
 
148
 
149
-  const [isLoggedIn, setIsLoggedIn] = useState(
150
-    ConnectionManager.getInstance().isLoggedIn()
151
-  );
149
+  const isLoggedIn = useLoginState();
152
   const { currentDashboard } = useCurrentDashboard();
150
   const { currentDashboard } = useCurrentDashboard();
153
 
151
 
154
   let homeDashboard: FullDashboardType | null = null;
152
   let homeDashboard: FullDashboardType | null = null;
199
           }
197
           }
200
         }
198
         }
201
       };
199
       };
202
-
203
-      if (ConnectionManager.getInstance().isLoggedIn() !== isLoggedIn) {
204
-        setIsLoggedIn(ConnectionManager.getInstance().isLoggedIn());
205
-      }
206
       // handle link open when home is not focused or created
200
       // handle link open when home is not focused or created
207
       handleNavigationParams();
201
       handleNavigationParams();
208
-      return () => {};
209
       // eslint-disable-next-line react-hooks/exhaustive-deps
202
       // eslint-disable-next-line react-hooks/exhaustive-deps
210
     }, [isLoggedIn])
203
     }, [isLoggedIn])
211
   );
204
   );

+ 32
- 2
src/utils/WebData.ts View File

80
 export async function apiRequest<T>(
80
 export async function apiRequest<T>(
81
   path: string,
81
   path: string,
82
   method: string,
82
   method: string,
83
-  params?: object
83
+  params?: object,
84
+  token?: string
84
 ): Promise<T> {
85
 ): Promise<T> {
85
   return new Promise(
86
   return new Promise(
86
     (resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
87
     (resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
88
       if (params != null) {
89
       if (params != null) {
89
         requestParams = { ...params };
90
         requestParams = { ...params };
90
       }
91
       }
91
-      console.log(Urls.amicale.api + path);
92
+      if (token) {
93
+        requestParams = { ...requestParams, token: token };
94
+      }
92
 
95
 
93
       fetch(Urls.amicale.api + path, {
96
       fetch(Urls.amicale.api + path, {
94
         method,
97
         method,
135
   );
138
   );
136
 }
139
 }
137
 
140
 
141
+export async function connectToAmicale(email: string, password: string) {
142
+  return new Promise(
143
+    (
144
+      resolve: (token: string) => void,
145
+      reject: (error: ApiRejectType) => void
146
+    ) => {
147
+      const data = {
148
+        email,
149
+        password,
150
+      };
151
+      apiRequest<ApiDataLoginType>('password', 'POST', data)
152
+        .then((response: ApiDataLoginType) => {
153
+          if (response.token != null) {
154
+            resolve(response.token);
155
+          } else {
156
+            reject({
157
+              status: REQUEST_STATUS.SERVER_ERROR,
158
+            });
159
+          }
160
+        })
161
+        .catch((err) => {
162
+          reject(err);
163
+        });
164
+    }
165
+  );
166
+}
167
+
138
 /**
168
 /**
139
  * Reads data from the given url and returns it.
169
  * Reads data from the given url and returns it.
140
  *
170
  *

+ 46
- 0
src/utils/loginToken.ts View File

1
+import * as Keychain from 'react-native-keychain';
2
+
3
+/**
4
+ * Tries to recover login token from the secure keychain
5
+ *
6
+ * @returns Promise<string | undefined>
7
+ */
8
+export async function retrieveLoginToken(): Promise<string | undefined> {
9
+  return new Promise((resolve: (token: string | undefined) => void) => {
10
+    Keychain.getGenericPassword()
11
+      .then((data: Keychain.UserCredentials | false) => {
12
+        if (data && data.password) {
13
+          resolve(data.password);
14
+        } else {
15
+          resolve(undefined);
16
+        }
17
+      })
18
+      .catch(() => resolve(undefined));
19
+  });
20
+}
21
+/**
22
+ * Saves the login token in the secure keychain
23
+ *
24
+ * @param email
25
+ * @param token
26
+ * @returns Promise<void>
27
+ */
28
+export async function saveLoginToken(
29
+  email: string,
30
+  token: string
31
+): Promise<void> {
32
+  return new Promise((resolve: () => void, reject: () => void) => {
33
+    Keychain.setGenericPassword(email, token).then(resolve).catch(reject);
34
+  });
35
+}
36
+
37
+/**
38
+ * Deletes the login token from the keychain
39
+ *
40
+ * @returns Promise<void>
41
+ */
42
+export async function deleteLoginToken(): Promise<void> {
43
+  return new Promise((resolve: () => void, reject: () => void) => {
44
+    Keychain.resetGenericPassword().then(resolve).catch(reject);
45
+  });
46
+}

+ 11
- 0
src/utils/logout.ts View File

1
+import { useCallback } from 'react';
2
+import { useLogin } from '../context/loginContext';
3
+
4
+export const useLogout = () => {
5
+  const { setLogin } = useLogin();
6
+
7
+  const onLogout = useCallback(() => {
8
+    setLogin(undefined);
9
+  }, [setLogin]);
10
+  return onLogout;
11
+};

Loading…
Cancel
Save