Browse Source

Improve Amicale screen components to match linter

Arnaud Vergnet 1 year ago
parent
commit
0a64f5fcd7

+ 138
- 118
src/screens/Amicale/AmicaleContactScreen.js View File

@@ -4,137 +4,157 @@ import * as React from 'react';
4 4
 import {FlatList, Image, Linking, View} from 'react-native';
5 5
 import {Card, List, Text, withTheme} from 'react-native-paper';
6 6
 import i18n from 'i18n-js';
7
-import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
8
-import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
7
+import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
8
+import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
9
+import AMICALE_LOGO from '../../../assets/amicale.png';
9 10
 
10
-type Props = {
11
+type DatasetItemType = {
12
+  name: string,
13
+  email: string,
14
+  icon: MaterialCommunityIconsGlyphs,
11 15
 };
12 16
 
13
-type DatasetItem = {
14
-    name: string,
15
-    email: string,
16
-    icon: MaterialCommunityIconsGlyphs,
17
-}
18
-
19 17
 /**
20 18
  * Class defining a planning event information page.
21 19
  */
22
-class AmicaleContactScreen extends React.Component<Props> {
23
-
24
-    // Dataset containing information about contacts
25
-    CONTACT_DATASET: Array<DatasetItem>;
20
+class AmicaleContactScreen extends React.Component<null> {
21
+  // Dataset containing information about contacts
22
+  CONTACT_DATASET: Array<DatasetItemType>;
26 23
 
27
-    constructor(props: Props) {
28
-        super(props);
29
-        this.CONTACT_DATASET = [
30
-            {
31
-                name: i18n.t("screens.amicaleAbout.roles.interSchools"),
32
-                email: "inter.ecoles@amicale-insat.fr",
33
-                icon: "share-variant"
34
-            },
35
-            {
36
-                name: i18n.t("screens.amicaleAbout.roles.culture"),
37
-                email: "culture@amicale-insat.fr",
38
-                icon: "book"
39
-            },
40
-            {
41
-                name: i18n.t("screens.amicaleAbout.roles.animation"),
42
-                email: "animation@amicale-insat.fr",
43
-                icon: "emoticon"
44
-            },
45
-            {
46
-                name: i18n.t("screens.amicaleAbout.roles.clubs"),
47
-                email: "clubs@amicale-insat.fr",
48
-                icon: "account-group"
49
-            },
50
-            {
51
-                name: i18n.t("screens.amicaleAbout.roles.event"),
52
-                email: "evenements@amicale-insat.fr",
53
-                icon: "calendar-range"
54
-            },
55
-            {
56
-                name: i18n.t("screens.amicaleAbout.roles.tech"),
57
-                email: "technique@amicale-insat.fr",
58
-                icon: "cog"
59
-            },
60
-            {
61
-                name: i18n.t("screens.amicaleAbout.roles.communication"),
62
-                email: "amicale@amicale-insat.fr",
63
-                icon: "comment-account"
64
-            },
65
-            {
66
-                name: i18n.t("screens.amicaleAbout.roles.intraSchools"),
67
-                email: "intra.ecoles@amicale-insat.fr",
68
-                icon: "school"
69
-            },
70
-            {
71
-                name: i18n.t("screens.amicaleAbout.roles.publicRelations"),
72
-                email: "rp@amicale-insat.fr",
73
-                icon: "account-tie"
74
-            },
75
-        ];
76
-    }
24
+  constructor() {
25
+    super();
26
+    this.CONTACT_DATASET = [
27
+      {
28
+        name: i18n.t('screens.amicaleAbout.roles.interSchools'),
29
+        email: 'inter.ecoles@amicale-insat.fr',
30
+        icon: 'share-variant',
31
+      },
32
+      {
33
+        name: i18n.t('screens.amicaleAbout.roles.culture'),
34
+        email: 'culture@amicale-insat.fr',
35
+        icon: 'book',
36
+      },
37
+      {
38
+        name: i18n.t('screens.amicaleAbout.roles.animation'),
39
+        email: 'animation@amicale-insat.fr',
40
+        icon: 'emoticon',
41
+      },
42
+      {
43
+        name: i18n.t('screens.amicaleAbout.roles.clubs'),
44
+        email: 'clubs@amicale-insat.fr',
45
+        icon: 'account-group',
46
+      },
47
+      {
48
+        name: i18n.t('screens.amicaleAbout.roles.event'),
49
+        email: 'evenements@amicale-insat.fr',
50
+        icon: 'calendar-range',
51
+      },
52
+      {
53
+        name: i18n.t('screens.amicaleAbout.roles.tech'),
54
+        email: 'technique@amicale-insat.fr',
55
+        icon: 'cog',
56
+      },
57
+      {
58
+        name: i18n.t('screens.amicaleAbout.roles.communication'),
59
+        email: 'amicale@amicale-insat.fr',
60
+        icon: 'comment-account',
61
+      },
62
+      {
63
+        name: i18n.t('screens.amicaleAbout.roles.intraSchools'),
64
+        email: 'intra.ecoles@amicale-insat.fr',
65
+        icon: 'school',
66
+      },
67
+      {
68
+        name: i18n.t('screens.amicaleAbout.roles.publicRelations'),
69
+        email: 'rp@amicale-insat.fr',
70
+        icon: 'account-tie',
71
+      },
72
+    ];
73
+  }
77 74
 
78
-    keyExtractor = (item: DatasetItem) => item.email;
75
+  keyExtractor = (item: DatasetItemType): string => item.email;
79 76
 
80
-    getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
77
+  getChevronIcon = ({
78
+    size,
79
+    color,
80
+  }: {
81
+    size: number,
82
+    color: string,
83
+  }): React.Node => (
84
+    <List.Icon size={size} color={color} icon="chevron-right" />
85
+  );
81 86
 
82
-    renderItem = ({item}: { item: DatasetItem }) => {
83
-        const onPress = () => Linking.openURL('mailto:' + item.email);
84
-        return <List.Item
85
-            title={item.name}
86
-            description={item.email}
87
-            left={(props) => <List.Icon {...props} icon={item.icon}/>}
88
-            right={this.getChevronIcon}
89
-            onPress={onPress}
90
-        />
87
+  getRenderItem = ({item}: {item: DatasetItemType}): React.Node => {
88
+    const onPress = () => {
89
+      Linking.openURL(`mailto:${item.email}`);
91 90
     };
91
+    return (
92
+      <List.Item
93
+        title={item.name}
94
+        description={item.email}
95
+        left={({size, color}: {size: number, color: string}): React.Node => (
96
+          <List.Icon size={size} color={color} icon={item.icon} />
97
+        )}
98
+        right={this.getChevronIcon}
99
+        onPress={onPress}
100
+      />
101
+    );
102
+  };
92 103
 
93
-    getScreen = () => {
94
-        return (
95
-            <View>
96
-                <View style={{
97
-                    width: '100%',
98
-                    height: 100,
99
-                    marginTop: 20,
100
-                    marginBottom: 20,
101
-                    justifyContent: 'center',
102
-                    alignItems: 'center'
103
-                }}>
104
-                    <Image
105
-                        source={require('../../../assets/amicale.png')}
106
-                        style={{flex: 1, resizeMode: "contain"}}
107
-                        resizeMode="contain"/>
108
-                </View>
109
-                <Card style={{margin: 5}}>
110
-                    <Card.Title
111
-                        title={i18n.t("screens.amicaleAbout.title")}
112
-                        subtitle={i18n.t("screens.amicaleAbout.subtitle")}
113
-                        left={props => <List.Icon {...props} icon={'information'}/>}
114
-                    />
115
-                    <Card.Content>
116
-                        <Text>{i18n.t("screens.amicaleAbout.message")}</Text>
117
-                        {/*$FlowFixMe*/}
118
-                        <FlatList
119
-                            data={this.CONTACT_DATASET}
120
-                            keyExtractor={this.keyExtractor}
121
-                            renderItem={this.renderItem}
122
-                        />
123
-                    </Card.Content>
124
-                </Card>
125
-            </View>
126
-        );
127
-    };
128
-
129
-    render() {
130
-        return (
131
-            <CollapsibleFlatList
132
-                data={[{key: "1"}]}
133
-                renderItem={this.getScreen}
134
-                hasTab={true}
104
+  getScreen = (): React.Node => {
105
+    return (
106
+      <View>
107
+        <View
108
+          style={{
109
+            width: '100%',
110
+            height: 100,
111
+            marginTop: 20,
112
+            marginBottom: 20,
113
+            justifyContent: 'center',
114
+            alignItems: 'center',
115
+          }}>
116
+          <Image
117
+            source={AMICALE_LOGO}
118
+            style={{flex: 1, resizeMode: 'contain'}}
119
+            resizeMode="contain"
120
+          />
121
+        </View>
122
+        <Card style={{margin: 5}}>
123
+          <Card.Title
124
+            title={i18n.t('screens.amicaleAbout.title')}
125
+            subtitle={i18n.t('screens.amicaleAbout.subtitle')}
126
+            left={({
127
+              size,
128
+              color,
129
+            }: {
130
+              size: number,
131
+              color: string,
132
+            }): React.Node => (
133
+              <List.Icon size={size} color={color} icon="information" />
134
+            )}
135
+          />
136
+          <Card.Content>
137
+            <Text>{i18n.t('screens.amicaleAbout.message')}</Text>
138
+            <FlatList
139
+              data={this.CONTACT_DATASET}
140
+              keyExtractor={this.keyExtractor}
141
+              renderItem={this.getRenderItem}
135 142
             />
136
-        );
137
-    }
143
+          </Card.Content>
144
+        </Card>
145
+      </View>
146
+    );
147
+  };
148
+
149
+  render(): React.Node {
150
+    return (
151
+      <CollapsibleFlatList
152
+        data={[{key: '1'}]}
153
+        renderItem={this.getScreen}
154
+        hasTab
155
+      />
156
+    );
157
+  }
138 158
 }
139 159
 
140 160
 export default withTheme(AmicaleContactScreen);

+ 2
- 2
src/screens/Amicale/Clubs/ClubDisplayScreen.js View File

@@ -17,7 +17,7 @@ import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen
17 17
 import CustomHTML from '../../../components/Overrides/CustomHTML';
18 18
 import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
19 19
 import type {ClubCategoryType, ClubType} from './ClubListScreen';
20
-import type {CustomTheme} from '../../../managers/ThemeManager';
20
+import type {CustomThemeType} from '../../../managers/ThemeManager';
21 21
 import {ERROR_TYPE} from '../../../utils/WebData';
22 22
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
23 23
 import type {ApiGenericDataType} from '../../../utils/WebData';
@@ -32,7 +32,7 @@ type PropsType = {
32 32
     },
33 33
     ...
34 34
   },
35
-  theme: CustomTheme,
35
+  theme: CustomThemeType,
36 36
 };
37 37
 
38 38
 const AMICALE_MAIL = 'clubs@amicale-insat.fr';

+ 2
- 2
src/screens/Amicale/Equipment/EquipmentConfirmScreen.js View File

@@ -11,7 +11,7 @@ import {
11 11
 } from 'react-native-paper';
12 12
 import {View} from 'react-native';
13 13
 import i18n from 'i18n-js';
14
-import type {CustomTheme} from '../../../managers/ThemeManager';
14
+import type {CustomThemeType} from '../../../managers/ThemeManager';
15 15
 import type {DeviceType} from './EquipmentListScreen';
16 16
 import {getRelativeDateString} from '../../../utils/EquipmentBooking';
17 17
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
@@ -23,7 +23,7 @@ type PropsType = {
23 23
       dates: [string, string],
24 24
     },
25 25
   },
26
-  theme: CustomTheme,
26
+  theme: CustomThemeType,
27 27
 };
28 28
 
29 29
 class EquipmentConfirmScreen extends React.Component<PropsType> {

+ 2
- 2
src/screens/Amicale/Equipment/EquipmentRentScreen.js View File

@@ -15,7 +15,7 @@ import * as Animatable from 'react-native-animatable';
15 15
 import i18n from 'i18n-js';
16 16
 import {CalendarList} from 'react-native-calendars';
17 17
 import type {DeviceType} from './EquipmentListScreen';
18
-import type {CustomTheme} from '../../../managers/ThemeManager';
18
+import type {CustomThemeType} from '../../../managers/ThemeManager';
19 19
 import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
20 20
 import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
21 21
 import {
@@ -36,7 +36,7 @@ type PropsType = {
36 36
       item?: DeviceType,
37 37
     },
38 38
   },
39
-  theme: CustomTheme,
39
+  theme: CustomThemeType,
40 40
 };
41 41
 
42 42
 export type MarkedDatesObjectType = {

+ 421
- 392
src/screens/Amicale/LoginScreen.js View File

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

+ 448
- 413
src/screens/Amicale/ProfileScreen.js View File

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

Loading…
Cancel
Save