Browse Source

Replace Authenticated screen by RequestScreen

Arnaud Vergnet 6 months ago
parent
commit
742643b9e2

+ 0
- 245
src/components/Amicale/AuthenticatedScreen.tsx View File

@@ -1,245 +0,0 @@
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 React from 'react';
21
-import { StackNavigationProp } from '@react-navigation/stack';
22
-import ConnectionManager from '../../managers/ConnectionManager';
23
-import { ERROR_TYPE } from '../../utils/WebData';
24
-import ErrorView from '../Screens/ErrorView';
25
-import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
26
-import i18n from 'i18n-js';
27
-
28
-type PropsType<T> = {
29
-  navigation: StackNavigationProp<any>;
30
-  requests: Array<{
31
-    link: string;
32
-    params: object;
33
-    mandatory: boolean;
34
-  }>;
35
-  renderFunction: (data: Array<T | null>) => React.ReactNode;
36
-  errorViewOverride?: Array<{
37
-    errorCode: number;
38
-    message: string;
39
-    icon: string;
40
-    showRetryButton: boolean;
41
-  }> | null;
42
-};
43
-
44
-type StateType = {
45
-  loading: boolean;
46
-};
47
-
48
-class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
49
-  static defaultProps = {
50
-    errorViewOverride: null,
51
-  };
52
-
53
-  currentUserToken: string | null;
54
-
55
-  connectionManager: ConnectionManager;
56
-
57
-  errors: Array<number>;
58
-
59
-  fetchedData: Array<T | null>;
60
-
61
-  constructor(props: PropsType<T>) {
62
-    super(props);
63
-    this.state = {
64
-      loading: true,
65
-    };
66
-    this.currentUserToken = null;
67
-    this.connectionManager = ConnectionManager.getInstance();
68
-    props.navigation.addListener('focus', this.onScreenFocus);
69
-    this.fetchedData = new Array(props.requests.length);
70
-    this.errors = new Array(props.requests.length);
71
-  }
72
-
73
-  /**
74
-   * Refreshes screen if user changed
75
-   */
76
-  onScreenFocus = () => {
77
-    if (this.currentUserToken !== this.connectionManager.getToken()) {
78
-      this.currentUserToken = this.connectionManager.getToken();
79
-      this.fetchData();
80
-    }
81
-  };
82
-
83
-  /**
84
-   * Callback used when a request finishes, successfully or not.
85
-   * Saves data and error code.
86
-   * If the token is invalid, logout the user and open the login screen.
87
-   * If the last request was received, stop the loading screen.
88
-   *
89
-   * @param data The data fetched from the server
90
-   * @param index The index for the data
91
-   * @param error The error code received
92
-   */
93
-  onRequestFinished(data: T | null, index: number, error?: number) {
94
-    const { props } = this;
95
-    if (index >= 0 && index < props.requests.length) {
96
-      this.fetchedData[index] = data;
97
-      this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
98
-    }
99
-    // Token expired, logout user
100
-    if (error === ERROR_TYPE.BAD_TOKEN) {
101
-      this.connectionManager.disconnect();
102
-    }
103
-
104
-    if (this.allRequestsFinished()) {
105
-      this.setState({ loading: false });
106
-    }
107
-  }
108
-
109
-  /**
110
-   * Gets the error to render.
111
-   * Non-mandatory requests are ignored.
112
-   *
113
-   *
114
-   * @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
115
-   */
116
-  getError(): number {
117
-    const { props } = this;
118
-    for (let i = 0; i < this.errors.length; i += 1) {
119
-      if (
120
-        this.errors[i] !== ERROR_TYPE.SUCCESS &&
121
-        props.requests[i].mandatory
122
-      ) {
123
-        return this.errors[i];
124
-      }
125
-    }
126
-    return ERROR_TYPE.SUCCESS;
127
-  }
128
-
129
-  /**
130
-   * Gets the error view to display in case of error
131
-   *
132
-   * @return {*}
133
-   */
134
-  getErrorRender() {
135
-    const { props } = this;
136
-    const errorCode = this.getError();
137
-    let shouldOverride = false;
138
-    let override = null;
139
-    const overrideList = props.errorViewOverride;
140
-    if (overrideList != null) {
141
-      for (let i = 0; i < overrideList.length; i += 1) {
142
-        if (overrideList[i].errorCode === errorCode) {
143
-          shouldOverride = true;
144
-          override = overrideList[i];
145
-          break;
146
-        }
147
-      }
148
-    }
149
-
150
-    if (shouldOverride && override != null) {
151
-      return (
152
-        <ErrorView
153
-          icon={override.icon}
154
-          message={override.message}
155
-          button={
156
-            override.showRetryButton
157
-              ? {
158
-                  icon: 'refresh',
159
-                  text: i18n.t('general.retry'),
160
-                  onPress: this.fetchData,
161
-                }
162
-              : undefined
163
-          }
164
-        />
165
-      );
166
-    }
167
-    return (
168
-      <ErrorView
169
-        status={errorCode}
170
-        button={{
171
-          icon: 'refresh',
172
-          text: i18n.t('general.retry'),
173
-          onPress: this.fetchData,
174
-        }}
175
-      />
176
-    );
177
-  }
178
-
179
-  /**
180
-   * Fetches the data from the server.
181
-   *
182
-   * If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
183
-   *
184
-   * If the user is logged in, send all requests.
185
-   */
186
-  fetchData = () => {
187
-    const { state, props } = this;
188
-    if (!state.loading) {
189
-      this.setState({ loading: true });
190
-    }
191
-
192
-    if (this.connectionManager.isLoggedIn()) {
193
-      for (let i = 0; i < props.requests.length; i += 1) {
194
-        this.connectionManager
195
-          .authenticatedRequest<T>(
196
-            props.requests[i].link,
197
-            props.requests[i].params
198
-          )
199
-          .then((response: T): void => this.onRequestFinished(response, i))
200
-          .catch((error: number): void =>
201
-            this.onRequestFinished(null, i, error)
202
-          );
203
-      }
204
-    } else {
205
-      for (let i = 0; i < props.requests.length; i += 1) {
206
-        this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
207
-      }
208
-    }
209
-  };
210
-
211
-  /**
212
-   * Checks if all requests finished processing
213
-   *
214
-   * @return {boolean} True if all finished
215
-   */
216
-  allRequestsFinished(): boolean {
217
-    let finished = true;
218
-    this.errors.forEach((error: number | null) => {
219
-      if (error == null) {
220
-        finished = false;
221
-      }
222
-    });
223
-    return finished;
224
-  }
225
-
226
-  /**
227
-   * Reloads the data, to be called using ref by parent components
228
-   */
229
-  reload() {
230
-    this.fetchData();
231
-  }
232
-
233
-  render() {
234
-    const { state, props } = this;
235
-    if (state.loading) {
236
-      return <BasicLoadingScreen />;
237
-    }
238
-    if (this.getError() === ERROR_TYPE.SUCCESS) {
239
-      return props.renderFunction(this.fetchedData);
240
-    }
241
-    return this.getErrorRender();
242
-  }
243
-}
244
-
245
-export default AuthenticatedScreen;

+ 1
- 1
src/components/Dialogs/ErrorDialog.tsx View File

@@ -39,7 +39,7 @@ function ErrorDialog(props: PropsType) {
39 39
       visible={props.visible}
40 40
       onDismiss={props.onDismiss}
41 41
       title={i18n.t('errors.title')}
42
-      message={getErrorMessage(props)}
42
+      message={getErrorMessage(props).message}
43 43
     />
44 44
   );
45 45
 }

+ 1
- 1
src/constants/Urls.tsx View File

@@ -33,7 +33,7 @@ const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/';
33 33
 
34 34
 export default {
35 35
   amicale: {
36
-    api: APP_ENDPOINT,
36
+    api: AMICALE_ENDPOINT,
37 37
     resetPassword: AMICALE_SERVER + 'password/reset',
38 38
     events: AMICALE_ENDPOINT + 'event/list',
39 39
   },

+ 4
- 2
src/managers/ConnectionManager.ts View File

@@ -163,7 +163,9 @@ export default class ConnectionManager {
163 163
               });
164 164
             }
165 165
           })
166
-          .catch(reject);
166
+          .catch((err) => {
167
+            reject(err);
168
+          });
167 169
       }
168 170
     );
169 171
   }
@@ -177,7 +179,7 @@ export default class ConnectionManager {
177 179
    */
178 180
   async authenticatedRequest<T>(
179 181
     path: string,
180
-    params: { [key: string]: any }
182
+    params?: { [key: string]: any }
181 183
   ): Promise<T> {
182 184
     return new Promise(
183 185
       (

+ 18
- 28
src/screens/Amicale/Clubs/ClubDisplayScreen.tsx View File

@@ -29,13 +29,13 @@ import {
29 29
 } from 'react-native-paper';
30 30
 import i18n from 'i18n-js';
31 31
 import { StackNavigationProp } from '@react-navigation/stack';
32
-import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
33 32
 import CustomHTML from '../../../components/Overrides/CustomHTML';
34 33
 import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
35 34
 import type { ClubCategoryType, ClubType } from './ClubListScreen';
36
-import { ERROR_TYPE } from '../../../utils/WebData';
37 35
 import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
38 36
 import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
37
+import RequestScreen from '../../../components/Screens/RequestScreen';
38
+import ConnectionManager from '../../../managers/ConnectionManager';
39 39
 
40 40
 type PropsType = {
41 41
   navigation: StackNavigationProp<any>;
@@ -49,6 +49,8 @@ type PropsType = {
49 49
   theme: ReactNativePaper.Theme;
50 50
 };
51 51
 
52
+type ResponseType = ClubType;
53
+
52 54
 const AMICALE_MAIL = 'clubs@amicale-insat.fr';
53 55
 
54 56
 const styles = StyleSheet.create({
@@ -88,7 +90,7 @@ const styles = StyleSheet.create({
88 90
  * If called with clubId parameter, will fetch the information on the server
89 91
  */
90 92
 class ClubDisplayScreen extends React.Component<PropsType> {
91
-  displayData: ClubType | null;
93
+  displayData: ClubType | undefined;
92 94
 
93 95
   categories: Array<ClubCategoryType> | null;
94 96
 
@@ -98,7 +100,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
98 100
 
99 101
   constructor(props: PropsType) {
100 102
     super(props);
101
-    this.displayData = null;
103
+    this.displayData = undefined;
102 104
     this.categories = null;
103 105
     this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
104 106
     this.shouldFetchData = true;
@@ -236,9 +238,8 @@ class ClubDisplayScreen extends React.Component<PropsType> {
236 238
     );
237 239
   }
238 240
 
239
-  getScreen = (response: Array<ClubType | null>) => {
240
-    let data: ClubType | null = response[0];
241
-    if (data != null) {
241
+  getScreen = (data: ResponseType | undefined) => {
242
+    if (data) {
242 243
       this.updateHeaderTitle(data);
243 244
       return (
244 245
         <CollapsibleScrollView style={styles.scroll} hasTab>
@@ -264,7 +265,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
264 265
         </CollapsibleScrollView>
265 266
       );
266 267
     }
267
-    return null;
268
+    return <View />;
268 269
   };
269 270
 
270 271
   /**
@@ -278,31 +279,20 @@ class ClubDisplayScreen extends React.Component<PropsType> {
278 279
   }
279 280
 
280 281
   render() {
281
-    const { props } = this;
282 282
     if (this.shouldFetchData) {
283 283
       return (
284
-        <AuthenticatedScreen
285
-          navigation={props.navigation}
286
-          requests={[
287
-            {
288
-              link: 'clubs/info',
289
-              params: { id: this.clubId },
290
-              mandatory: true,
291
-            },
292
-          ]}
293
-          renderFunction={this.getScreen}
294
-          errorViewOverride={[
295
-            {
296
-              errorCode: ERROR_TYPE.BAD_INPUT,
297
-              message: i18n.t('screens.clubs.invalidClub'),
298
-              icon: 'account-question',
299
-              showRetryButton: false,
300
-            },
301
-          ]}
284
+        <RequestScreen
285
+          request={() =>
286
+            ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
287
+              'clubs/info',
288
+              { id: this.clubId }
289
+            )
290
+          }
291
+          render={this.getScreen}
302 292
         />
303 293
       );
304 294
     }
305
-    return this.getScreen([this.displayData]);
295
+    return this.getScreen(this.displayData);
306 296
   }
307 297
 }
308 298
 

+ 38
- 53
src/screens/Amicale/Clubs/ClubListScreen.tsx View File

@@ -22,7 +22,6 @@ import { Platform } from 'react-native';
22 22
 import { Searchbar } from 'react-native-paper';
23 23
 import i18n from 'i18n-js';
24 24
 import { StackNavigationProp } from '@react-navigation/stack';
25
-import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
26 25
 import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
27 26
 import {
28 27
   isItemInCategoryFilter,
@@ -32,7 +31,8 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
32 31
 import MaterialHeaderButtons, {
33 32
   Item,
34 33
 } from '../../../components/Overrides/CustomHeaderButton';
35
-import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
34
+import ConnectionManager from '../../../managers/ConnectionManager';
35
+import WebSectionList from '../../../components/Screens/WebSectionList';
36 36
 
37 37
 export type ClubCategoryType = {
38 38
   id: number;
@@ -58,6 +58,11 @@ type StateType = {
58 58
   currentSearchString: string;
59 59
 };
60 60
 
61
+type ResponseType = {
62
+  categories: Array<ClubCategoryType>;
63
+  clubs: Array<ClubType>;
64
+};
65
+
61 66
 const LIST_ITEM_HEIGHT = 96;
62 67
 
63 68
 class ClubListScreen extends React.Component<PropsType, StateType> {
@@ -146,30 +151,13 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
146 151
     );
147 152
   };
148 153
 
149
-  getScreen = (
150
-    data: Array<{
151
-      categories: Array<ClubCategoryType>;
152
-      clubs: Array<ClubType>;
153
-    } | null>
154
-  ) => {
155
-    let categoryList: Array<ClubCategoryType> = [];
156
-    let clubList: Array<ClubType> = [];
157
-    if (data[0] != null) {
158
-      categoryList = data[0].categories;
159
-      clubList = data[0].clubs;
154
+  createDataset = (data: ResponseType | undefined) => {
155
+    if (data) {
156
+      this.categories = data?.categories;
157
+      return [{ title: '', data: data.clubs }];
158
+    } else {
159
+      return [{ title: '', data: [] }];
160 160
     }
161
-    this.categories = categoryList;
162
-    return (
163
-      <CollapsibleFlatList
164
-        data={clubList}
165
-        keyExtractor={this.keyExtractor}
166
-        renderItem={this.getRenderItem}
167
-        ListHeaderComponent={this.getListHeader()}
168
-        // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
169
-        removeClippedSubviews
170
-        getItemLayout={this.itemLayout}
171
-      />
172
-    );
173 161
   };
174 162
 
175 163
   /**
@@ -177,15 +165,19 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
177 165
    *
178 166
    * @returns {*}
179 167
    */
180
-  getListHeader() {
168
+  getListHeader(data: ResponseType | undefined) {
181 169
     const { state } = this;
182
-    return (
183
-      <ClubListHeader
184
-        categories={this.categories}
185
-        selectedCategories={state.currentlySelectedCategories}
186
-        onChipSelect={this.onChipSelect}
187
-      />
188
-    );
170
+    if (data) {
171
+      return (
172
+        <ClubListHeader
173
+          categories={this.categories}
174
+          selectedCategories={state.currentlySelectedCategories}
175
+          onChipSelect={this.onChipSelect}
176
+        />
177
+      );
178
+    } else {
179
+      return null;
180
+    }
189 181
   }
190 182
 
191 183
   /**
@@ -223,15 +215,6 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
223 215
 
224 216
   keyExtractor = (item: ClubType): string => item.id.toString();
225 217
 
226
-  itemLayout = (
227
-    _data: Array<ClubType> | null | undefined,
228
-    index: number
229
-  ): { length: number; offset: number; index: number } => ({
230
-    length: LIST_ITEM_HEIGHT,
231
-    offset: LIST_ITEM_HEIGHT * index,
232
-    index,
233
-  });
234
-
235 218
   /**
236 219
    * Updates the search string and category filter, saving them to the State.
237 220
    *
@@ -282,18 +265,20 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
282 265
   }
283 266
 
284 267
   render() {
285
-    const { props } = this;
286 268
     return (
287
-      <AuthenticatedScreen
288
-        navigation={props.navigation}
289
-        requests={[
290
-          {
291
-            link: 'clubs/list',
292
-            params: {},
293
-            mandatory: true,
294
-          },
295
-        ]}
296
-        renderFunction={this.getScreen}
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}
297 282
       />
298 283
     );
299 284
   }

+ 55
- 67
src/screens/Amicale/Equipment/EquipmentListScreen.tsx View File

@@ -22,13 +22,14 @@ import { StyleSheet, View } from 'react-native';
22 22
 import { Button } from 'react-native-paper';
23 23
 import { StackNavigationProp } from '@react-navigation/stack';
24 24
 import i18n from 'i18n-js';
25
-import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
26 25
 import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
27 26
 import MascotPopup from '../../../components/Mascot/MascotPopup';
28 27
 import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
29 28
 import AsyncStorageManager from '../../../managers/AsyncStorageManager';
30
-import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
31 29
 import GENERAL_STYLES from '../../../constants/Styles';
30
+import ConnectionManager from '../../../managers/ConnectionManager';
31
+import { ApiRejectType } from '../../../utils/WebData';
32
+import WebSectionList from '../../../components/Screens/WebSectionList';
32 33
 
33 34
 type PropsType = {
34 35
   navigation: StackNavigationProp<any>;
@@ -52,6 +53,11 @@ export type RentedDeviceType = {
52 53
   end: string;
53 54
 };
54 55
 
56
+type ResponseType = {
57
+  devices: Array<DeviceType>;
58
+  locations?: Array<RentedDeviceType>;
59
+};
60
+
55 61
 const LIST_ITEM_HEIGHT = 64;
56 62
 
57 63
 const styles = StyleSheet.create({
@@ -65,10 +71,6 @@ const styles = StyleSheet.create({
65 71
 class EquipmentListScreen extends React.Component<PropsType, StateType> {
66 72
   userRents: null | Array<RentedDeviceType>;
67 73
 
68
-  authRef: { current: null | AuthenticatedScreen<any> };
69
-
70
-  canRefresh: boolean;
71
-
72 74
   constructor(props: PropsType) {
73 75
     super(props);
74 76
     this.userRents = null;
@@ -77,22 +79,8 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
77 79
         AsyncStorageManager.PREFERENCES.equipmentShowMascot.key
78 80
       ),
79 81
     };
80
-    this.canRefresh = false;
81
-    this.authRef = React.createRef();
82
-    props.navigation.addListener('focus', this.onScreenFocus);
83 82
   }
84 83
 
85
-  onScreenFocus = () => {
86
-    if (
87
-      this.canRefresh &&
88
-      this.authRef.current &&
89
-      this.authRef.current.reload
90
-    ) {
91
-      this.authRef.current.reload();
92
-    }
93
-    this.canRefresh = true;
94
-  };
95
-
96 84
   getRenderItem = ({ item }: { item: DeviceType }) => {
97 85
     const { navigation } = this.props;
98 86
     return (
@@ -139,37 +127,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
139 127
 
140 128
   keyExtractor = (item: DeviceType): string => item.id.toString();
141 129
 
142
-  /**
143
-   * Gets the main screen component with the fetched data
144
-   *
145
-   * @param data The data fetched from the server
146
-   * @returns {*}
147
-   */
148
-  getScreen = (
149
-    data: Array<
150
-      | { devices: Array<DeviceType> }
151
-      | { locations: Array<RentedDeviceType> }
152
-      | null
153
-    >
154
-  ) => {
155
-    const [allDevices, userRents] = data;
156
-    if (userRents) {
157
-      this.userRents = (userRents as {
158
-        locations: Array<RentedDeviceType>;
159
-      }).locations;
130
+  createDataset = (data: ResponseType | undefined) => {
131
+    if (data) {
132
+      const userRents = data.locations;
133
+
134
+      if (userRents) {
135
+        this.userRents = userRents;
136
+      }
137
+      return [{ title: '', data: data.devices }];
138
+    } else {
139
+      return [{ title: '', data: [] }];
160 140
     }
161
-    return (
162
-      <CollapsibleFlatList
163
-        keyExtractor={this.keyExtractor}
164
-        renderItem={this.getRenderItem}
165
-        ListHeaderComponent={this.getListHeader()}
166
-        data={
167
-          allDevices
168
-            ? (allDevices as { devices: Array<DeviceType> }).devices
169
-            : null
170
-        }
171
-      />
172
-    );
173 141
   };
174 142
 
175 143
   showMascotDialog = () => {
@@ -184,26 +152,46 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
184 152
     this.setState({ mascotDialogVisible: false });
185 153
   };
186 154
 
155
+  request = () => {
156
+    return new Promise(
157
+      (
158
+        resolve: (data: ResponseType) => void,
159
+        reject: (error: ApiRejectType) => void
160
+      ) => {
161
+        ConnectionManager.getInstance()
162
+          .authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
163
+          .then((devicesData) => {
164
+            ConnectionManager.getInstance()
165
+              .authenticatedRequest<{
166
+                locations: Array<RentedDeviceType>;
167
+              }>('location/my')
168
+              .then((rentsData) => {
169
+                resolve({
170
+                  devices: devicesData.devices,
171
+                  locations: rentsData.locations,
172
+                });
173
+              })
174
+              .catch(() =>
175
+                resolve({
176
+                  devices: devicesData.devices,
177
+                })
178
+              );
179
+          })
180
+          .catch(reject);
181
+      }
182
+    );
183
+  };
184
+
187 185
   render() {
188
-    const { props, state } = this;
186
+    const { state } = this;
189 187
     return (
190 188
       <View style={GENERAL_STYLES.flex}>
191
-        <AuthenticatedScreen
192
-          navigation={props.navigation}
193
-          ref={this.authRef}
194
-          requests={[
195
-            {
196
-              link: 'location/all',
197
-              params: {},
198
-              mandatory: true,
199
-            },
200
-            {
201
-              link: 'location/my',
202
-              params: {},
203
-              mandatory: false,
204
-            },
205
-          ]}
206
-          renderFunction={this.getScreen}
189
+        <WebSectionList
190
+          request={this.request}
191
+          createDataset={this.createDataset}
192
+          keyExtractor={this.keyExtractor}
193
+          renderItem={this.getRenderItem}
194
+          renderListHeaderComponent={() => this.getListHeader()}
207 195
         />
208 196
         <MascotPopup
209 197
           visible={state.mascotDialogVisible}

+ 10
- 6
src/screens/Amicale/LoginScreen.tsx View File

@@ -55,7 +55,7 @@ type StateType = {
55 55
   isPasswordValidated: boolean;
56 56
   loading: boolean;
57 57
   dialogVisible: boolean;
58
-  dialogError: REQUEST_STATUS;
58
+  dialogError: ApiRejectType;
59 59
   mascotDialogVisible: boolean;
60 60
 };
61 61
 
@@ -110,14 +110,15 @@ class LoginScreen extends React.Component<Props, StateType> {
110 110
       this.onInputChange(false, value);
111 111
     };
112 112
     props.navigation.addListener('focus', this.onScreenFocus);
113
+    // TODO remove
113 114
     this.state = {
114
-      email: '',
115
-      password: '',
115
+      email: 'vergnet@etud.insa-toulouse.fr',
116
+      password: 'IGtt25ùj',
116 117
       isEmailValidated: false,
117 118
       isPasswordValidated: false,
118 119
       loading: false,
119 120
       dialogVisible: false,
120
-      dialogError: REQUEST_STATUS.SUCCESS,
121
+      dialogError: { status: REQUEST_STATUS.SUCCESS },
121 122
       mascotDialogVisible: AsyncStorageManager.getBool(
122 123
         AsyncStorageManager.PREFERENCES.loginShowMascot.key
123 124
       ),
@@ -338,9 +339,11 @@ class LoginScreen extends React.Component<Props, StateType> {
338 339
    * @param error The error given by the login request
339 340
    */
340 341
   showErrorDialog = (error: ApiRejectType) => {
342
+    console.log(error);
343
+
341 344
     this.setState({
342 345
       dialogVisible: true,
343
-      dialogError: error.status,
346
+      dialogError: error,
344 347
     });
345 348
   };
346 349
 
@@ -460,7 +463,8 @@ class LoginScreen extends React.Component<Props, StateType> {
460 463
             <ErrorDialog
461 464
               visible={dialogVisible}
462 465
               onDismiss={this.hideErrorDialog}
463
-              status={dialogError}
466
+              status={dialogError.status}
467
+              code={dialogError.code}
464 468
             />
465 469
           </CollapsibleScrollView>
466 470
         </KeyboardAvoidingView>

+ 27
- 28
src/screens/Amicale/ProfileScreen.tsx View File

@@ -30,7 +30,6 @@ import {
30 30
 } from 'react-native-paper';
31 31
 import i18n from 'i18n-js';
32 32
 import { StackNavigationProp } from '@react-navigation/stack';
33
-import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
34 33
 import LogoutDialog from '../../components/Amicale/LogoutDialog';
35 34
 import MaterialHeaderButtons, {
36 35
   Item,
@@ -42,6 +41,8 @@ import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatLis
42 41
 import type { ServiceItemType } from '../../managers/ServicesManager';
43 42
 import GENERAL_STYLES from '../../constants/Styles';
44 43
 import Urls from '../../constants/Urls';
44
+import RequestScreen from '../../components/Screens/RequestScreen';
45
+import ConnectionManager from '../../managers/ConnectionManager';
45 46
 
46 47
 type PropsType = {
47 48
   navigation: StackNavigationProp<any>;
@@ -89,7 +90,7 @@ const styles = StyleSheet.create({
89 90
 });
90 91
 
91 92
 class ProfileScreen extends React.Component<PropsType, StateType> {
92
-  data: ProfileDataType | null;
93
+  data: ProfileDataType | undefined;
93 94
 
94 95
   flatListData: Array<{ id: string }>;
95 96
 
@@ -97,7 +98,7 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
97 98
 
98 99
   constructor(props: PropsType) {
99 100
     super(props);
100
-    this.data = null;
101
+    this.data = undefined;
101 102
     this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
102 103
     const services = new ServicesManager(props.navigation);
103 104
     this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
@@ -134,21 +135,25 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
134 135
    * @param data The data fetched from the server
135 136
    * @returns {*}
136 137
    */
137
-  getScreen = (data: Array<ProfileDataType | null>) => {
138
+  getScreen = (data: ProfileDataType | undefined) => {
138 139
     const { dialogVisible } = this.state;
139
-    this.data = data[0];
140
-    return (
141
-      <View style={GENERAL_STYLES.flex}>
142
-        <CollapsibleFlatList
143
-          renderItem={this.getRenderItem}
144
-          data={this.flatListData}
145
-        />
146
-        <LogoutDialog
147
-          visible={dialogVisible}
148
-          onDismiss={this.hideDisconnectDialog}
149
-        />
150
-      </View>
151
-    );
140
+    if (data) {
141
+      this.data = data;
142
+      return (
143
+        <View style={GENERAL_STYLES.flex}>
144
+          <CollapsibleFlatList
145
+            renderItem={this.getRenderItem}
146
+            data={this.flatListData}
147
+          />
148
+          <LogoutDialog
149
+            visible={dialogVisible}
150
+            onDismiss={this.hideDisconnectDialog}
151
+          />
152
+        </View>
153
+      );
154
+    } else {
155
+      return <View />;
156
+    }
152 157
   };
153 158
 
154 159
   getRenderItem = ({ item }: { item: { id: string } }) => {
@@ -482,18 +487,12 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
482 487
   }
483 488
 
484 489
   render() {
485
-    const { navigation } = this.props;
486 490
     return (
487
-      <AuthenticatedScreen
488
-        navigation={navigation}
489
-        requests={[
490
-          {
491
-            link: 'user/profile',
492
-            params: {},
493
-            mandatory: true,
494
-          },
495
-        ]}
496
-        renderFunction={this.getScreen}
491
+      <RequestScreen<ProfileDataType>
492
+        request={() =>
493
+          ConnectionManager.getInstance().authenticatedRequest('user/profile')
494
+        }
495
+        render={this.getScreen}
497 496
       />
498 497
     );
499 498
   }

+ 73
- 82
src/screens/Amicale/VoteScreen.tsx View File

@@ -18,11 +18,9 @@
18 18
  */
19 19
 
20 20
 import * as React from 'react';
21
-import { RefreshControl, StyleSheet, View } from 'react-native';
22
-import { StackNavigationProp } from '@react-navigation/stack';
21
+import { StyleSheet, View } from 'react-native';
23 22
 import i18n from 'i18n-js';
24 23
 import { Button } from 'react-native-paper';
25
-import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
26 24
 import { getTimeOnlyString, stringToDate } from '../../utils/Planning';
27 25
 import VoteTease from '../../components/Amicale/Vote/VoteTease';
28 26
 import VoteSelect from '../../components/Amicale/Vote/VoteSelect';
@@ -32,8 +30,11 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
32 30
 import MascotPopup from '../../components/Mascot/MascotPopup';
33 31
 import AsyncStorageManager from '../../managers/AsyncStorageManager';
34 32
 import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
35
-import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
36 33
 import GENERAL_STYLES from '../../constants/Styles';
34
+import ConnectionManager from '../../managers/ConnectionManager';
35
+import WebSectionList, {
36
+  SectionListDataType,
37
+} from '../../components/Screens/WebSectionList';
37 38
 
38 39
 export type VoteTeamType = {
39 40
   id: number;
@@ -60,6 +61,11 @@ type VoteDatesObjectType = {
60 61
   date_result_end: Date;
61 62
 };
62 63
 
64
+type ResponseType = {
65
+  teams?: TeamResponseType;
66
+  dates?: VoteDatesStringType;
67
+};
68
+
63 69
 // const FAKE_DATE = {
64 70
 //     "date_begin": "2020-08-19 15:50",
65 71
 //     "date_end": "2020-08-19 15:50",
@@ -108,11 +114,7 @@ type VoteDatesObjectType = {
108 114
 //     ],
109 115
 // };
110 116
 
111
-const MIN_REFRESH_TIME = 5 * 1000;
112
-
113
-type PropsType = {
114
-  navigation: StackNavigationProp<any>;
115
-};
117
+type PropsType = {};
116 118
 
117 119
 type StateType = {
118 120
   hasVoted: boolean;
@@ -135,23 +137,21 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
135 137
 
136 138
   hasVoted: boolean;
137 139
 
138
-  datesString: null | VoteDatesStringType;
140
+  datesString: undefined | VoteDatesStringType;
139 141
 
140
-  dates: null | VoteDatesObjectType;
142
+  dates: undefined | VoteDatesObjectType;
141 143
 
142 144
   today: Date;
143 145
 
144
-  mainFlatListData: Array<{ key: string }>;
146
+  mainFlatListData: SectionListDataType<{ key: string }>;
145 147
 
146
-  lastRefresh: Date | null;
147
-
148
-  authRef: { current: null | AuthenticatedScreen<any> };
148
+  refreshData: () => void;
149 149
 
150 150
   constructor(props: PropsType) {
151 151
     super(props);
152 152
     this.teams = [];
153
-    this.datesString = null;
154
-    this.dates = null;
153
+    this.datesString = undefined;
154
+    this.dates = undefined;
155 155
     this.state = {
156 156
       hasVoted: false,
157 157
       mascotDialogVisible: AsyncStorageManager.getBool(
@@ -160,9 +160,10 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
160 160
     };
161 161
     this.hasVoted = false;
162 162
     this.today = new Date();
163
-    this.authRef = React.createRef();
164
-    this.lastRefresh = null;
165
-    this.mainFlatListData = [{ key: 'main' }, { key: 'info' }];
163
+    this.refreshData = () => undefined;
164
+    this.mainFlatListData = [
165
+      { title: '', data: [{ key: 'main' }, { key: 'info' }] },
166
+    ];
166 167
   }
167 168
 
168 169
   /**
@@ -201,37 +202,32 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
201 202
     return this.getContent();
202 203
   };
203 204
 
204
-  getScreen = (data: Array<TeamResponseType | VoteDatesStringType | null>) => {
205
-    const { state } = this;
205
+  createDataset = (
206
+    data: ResponseType | undefined,
207
+    _loading: boolean,
208
+    _lastRefreshDate: Date | undefined,
209
+    refreshData: () => void
210
+  ) => {
206 211
     // data[0] = FAKE_TEAMS2;
207 212
     // data[1] = FAKE_DATE;
208
-    this.lastRefresh = new Date();
213
+    this.refreshData = refreshData;
214
+    if (data) {
215
+      const { teams, dates } = data;
209 216
 
210
-    const teams = data[0] as TeamResponseType | null;
211
-    const dateStrings = data[1] as VoteDatesStringType | null;
217
+      if (dates && dates.date_begin == null) {
218
+        this.datesString = undefined;
219
+      } else {
220
+        this.datesString = dates;
221
+      }
212 222
 
213
-    if (dateStrings != null && dateStrings.date_begin == null) {
214
-      this.datesString = null;
215
-    } else {
216
-      this.datesString = dateStrings;
217
-    }
223
+      if (teams) {
224
+        this.teams = teams.teams;
225
+        this.hasVoted = teams.has_voted;
226
+      }
218 227
 
219
-    if (teams != null) {
220
-      this.teams = teams.teams;
221
-      this.hasVoted = teams.has_voted;
228
+      this.generateDateObject();
222 229
     }
223
-
224
-    this.generateDateObject();
225
-    return (
226
-      <CollapsibleFlatList
227
-        data={this.mainFlatListData}
228
-        refreshControl={
229
-          <RefreshControl refreshing={false} onRefresh={this.reloadData} />
230
-        }
231
-        extraData={state.hasVoted.toString()}
232
-        renderItem={this.getMainRenderItem}
233
-      />
234
-    );
230
+    return this.mainFlatListData;
235 231
   };
236 232
 
237 233
   getContent() {
@@ -261,7 +257,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
261 257
       <VoteSelect
262 258
         teams={this.teams}
263 259
         onVoteSuccess={this.onVoteSuccess}
264
-        onVoteError={this.reloadData}
260
+        onVoteError={this.refreshData}
265 261
       />
266 262
     );
267 263
   }
@@ -327,23 +323,6 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
327 323
     );
328 324
   }
329 325
 
330
-  /**
331
-   * Reloads vote data if last refresh delta is smaller than the minimum refresh time
332
-   */
333
-  reloadData = () => {
334
-    let canRefresh;
335
-    const { lastRefresh } = this;
336
-    if (lastRefresh != null) {
337
-      canRefresh =
338
-        new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
339
-    } else {
340
-      canRefresh = true;
341
-    }
342
-    if (canRefresh && this.authRef.current != null) {
343
-      this.authRef.current.reload();
344
-    }
345
-  };
346
-
347 326
   showMascotDialog = () => {
348 327
     this.setState({ mascotDialogVisible: true });
349 328
   };
@@ -403,13 +382,36 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
403 382
           date_result_end: dateResultEnd,
404 383
         };
405 384
       } else {
406
-        this.dates = null;
385
+        this.dates = undefined;
407 386
       }
408 387
     } else {
409
-      this.dates = null;
388
+      this.dates = undefined;
410 389
     }
411 390
   }
412 391
 
392
+  request = () => {
393
+    return new Promise((resolve: (data: ResponseType) => void) => {
394
+      ConnectionManager.getInstance()
395
+        .authenticatedRequest<VoteDatesStringType>('elections/dates')
396
+        .then((datesData) => {
397
+          ConnectionManager.getInstance()
398
+            .authenticatedRequest<TeamResponseType>('elections/teams')
399
+            .then((teamsData) => {
400
+              resolve({
401
+                dates: datesData,
402
+                teams: teamsData,
403
+              });
404
+            })
405
+            .catch(() =>
406
+              resolve({
407
+                dates: datesData,
408
+              })
409
+            );
410
+        })
411
+        .catch(() => resolve({}));
412
+    });
413
+  };
414
+
413 415
   /**
414 416
    * Renders the authenticated screen.
415 417
    *
@@ -418,25 +420,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
418 420
    * @returns {*}
419 421
    */
420 422
   render() {
421
-    const { props, state } = this;
423
+    const { state } = this;
422 424
     return (
423 425
       <View style={GENERAL_STYLES.flex}>
424
-        <AuthenticatedScreen<TeamResponseType | VoteDatesStringType>
425
-          navigation={props.navigation}
426
-          ref={this.authRef}
427
-          requests={[
428
-            {
429
-              link: 'elections/teams',
430
-              params: {},
431
-              mandatory: false,
432
-            },
433
-            {
434
-              link: 'elections/dates',
435
-              params: {},
436
-              mandatory: false,
437
-            },
438
-          ]}
439
-          renderFunction={this.getScreen}
426
+        <WebSectionList
427
+          request={this.request}
428
+          createDataset={this.createDataset}
429
+          extraData={state.hasVoted.toString()}
430
+          renderItem={this.getMainRenderItem}
440 431
         />
441 432
         <MascotPopup
442 433
           visible={state.mascotDialogVisible}

+ 23
- 12
src/utils/WebData.ts View File

@@ -39,8 +39,9 @@ export type ApiDataLoginType = {
39 39
 };
40 40
 
41 41
 type ApiResponseType<T> = {
42
-  error: number;
43
-  data: T;
42
+  status: REQUEST_STATUS;
43
+  error?: API_REQUEST_CODES;
44
+  data?: T;
44 45
 };
45 46
 
46 47
 export type ApiRejectType = {
@@ -87,6 +88,8 @@ export async function apiRequest<T>(
87 88
       if (params != null) {
88 89
         requestParams = { ...params };
89 90
       }
91
+      console.log(Urls.amicale.api + path);
92
+
90 93
       fetch(Urls.amicale.api + path, {
91 94
         method,
92 95
         headers: new Headers({
@@ -95,12 +98,20 @@ export async function apiRequest<T>(
95 98
         }),
96 99
         body: JSON.stringify(requestParams),
97 100
       })
98
-        .then(
99
-          async (response: Response): Promise<ApiResponseType<T>> =>
100
-            response.json()
101
-        )
101
+        .then((response: Response) => {
102
+          const status = response.status;
103
+          if (status === REQUEST_STATUS.SUCCESS) {
104
+            return response.json().then(
105
+              (data): ApiResponseType<T> => {
106
+                return { status: status, error: data.error, data: data.data };
107
+              }
108
+            );
109
+          } else {
110
+            return { status: status };
111
+          }
112
+        })
102 113
         .then((response: ApiResponseType<T>) => {
103
-          if (isApiResponseValid(response)) {
114
+          if (isApiResponseValid(response) && response.data) {
104 115
             if (response.error === API_REQUEST_CODES.SUCCESS) {
105 116
               resolve(response.data);
106 117
             } else {
@@ -111,15 +122,15 @@ export async function apiRequest<T>(
111 122
             }
112 123
           } else {
113 124
             reject({
114
-              status: REQUEST_STATUS.SERVER_ERROR,
125
+              status: response.status,
115 126
             });
116 127
           }
117 128
         })
118
-        .catch(() =>
129
+        .catch(() => {
119 130
           reject({
120
-            status: REQUEST_STATUS.SERVER_ERROR,
121
-          })
122
-        );
131
+            status: REQUEST_STATUS.CONNECTION_ERROR,
132
+          });
133
+        });
123 134
     }
124 135
   );
125 136
 }

Loading…
Cancel
Save