Browse Source

Improve requests handlers to match linter

Arnaud Vergnet 3 years ago
parent
commit
0a9e0eb0ca
2 changed files with 273 additions and 279 deletions
  1. 190
    204
      src/components/Amicale/AuthenticatedScreen.js
  2. 83
    75
      src/utils/WebData.js

+ 190
- 204
src/components/Amicale/AuthenticatedScreen.js View File

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

+ 83
- 75
src/utils/WebData.js View File

@@ -1,66 +1,32 @@
1 1
 // @flow
2 2
 
3 3
 export const ERROR_TYPE = {
4
-    SUCCESS: 0,
5
-    BAD_CREDENTIALS: 1,
6
-    BAD_TOKEN: 2,
7
-    NO_CONSENT: 3,
8
-    TOKEN_SAVE: 4,
9
-    TOKEN_RETRIEVE: 5,
10
-    BAD_INPUT: 400,
11
-    FORBIDDEN: 403,
12
-    CONNECTION_ERROR: 404,
13
-    SERVER_ERROR: 500,
14
-    UNKNOWN: 999,
4
+  SUCCESS: 0,
5
+  BAD_CREDENTIALS: 1,
6
+  BAD_TOKEN: 2,
7
+  NO_CONSENT: 3,
8
+  TOKEN_SAVE: 4,
9
+  TOKEN_RETRIEVE: 5,
10
+  BAD_INPUT: 400,
11
+  FORBIDDEN: 403,
12
+  CONNECTION_ERROR: 404,
13
+  SERVER_ERROR: 500,
14
+  UNKNOWN: 999,
15 15
 };
16 16
 
17
-type response_format = {
18
-    error: number,
19
-    data: Object,
20
-}
17
+export type ApiDataLoginType = {
18
+  token: string,
19
+};
21 20
 
22
-const API_ENDPOINT = "https://www.amicale-insat.fr/api/";
21
+// eslint-disable-next-line flowtype/no-weak-types
22
+export type ApiGenericDataType = {[key: string]: any};
23 23
 
24
-/**
25
- * Sends a request to the Amicale Website backend
26
- *
27
- * In case of failure, the promise will be rejected with the error code.
28
- * In case of success, the promise will return the data object.
29
- *
30
- * @param path The API path from the API endpoint
31
- * @param method The HTTP method to use (GET or POST)
32
- * @param params The params to use for this request
33
- * @returns {Promise<R>}
34
- */
35
-export async function apiRequest(path: string, method: string, params: ?{ [key: string]: string | number }) {
36
-    if (params === undefined || params === null)
37
-        params = {};
24
+type ApiResponseType = {
25
+  error: number,
26
+  data: ApiGenericDataType,
27
+};
38 28
 
39
-    return new Promise((resolve, reject) => {
40
-        fetch(API_ENDPOINT + path, {
41
-            method: method,
42
-            headers: new Headers({
43
-                'Accept': 'application/json',
44
-                'Content-Type': 'application/json',
45
-            }),
46
-            body: JSON.stringify({
47
-                ...params
48
-            })
49
-        }).then(async (response) => response.json())
50
-            .then((response: response_format) => {
51
-                if (isResponseValid(response)) {
52
-                    if (response.error === ERROR_TYPE.SUCCESS)
53
-                        resolve(response.data);
54
-                    else
55
-                        reject(response.error);
56
-                } else
57
-                    reject(ERROR_TYPE.SERVER_ERROR);
58
-            })
59
-            .catch(() => {
60
-                reject(ERROR_TYPE.CONNECTION_ERROR);
61
-            });
62
-    });
63
-}
29
+const API_ENDPOINT = 'https://www.amicale-insat.fr/api/';
64 30
 
65 31
 /**
66 32
  * Checks if the given API response is valid.
@@ -70,15 +36,59 @@ export async function apiRequest(path: string, method: string, params: ?{ [key:
70 36
  * @param response
71 37
  * @returns {boolean}
72 38
  */
73
-export function isResponseValid(response: response_format) {
74
-    let valid = response !== undefined
75
-        && response.error !== undefined
76
-        && typeof response.error === "number";
39
+export function isApiResponseValid(response: ApiResponseType): boolean {
40
+  return (
41
+    response != null &&
42
+    response.error != null &&
43
+    typeof response.error === 'number' &&
44
+    response.data != null &&
45
+    typeof response.data === 'object'
46
+  );
47
+}
77 48
 
78
-    valid = valid
79
-        && response.data !== undefined
80
-        && typeof response.data === "object";
81
-    return valid;
49
+/**
50
+ * Sends a request to the Amicale Website backend
51
+ *
52
+ * In case of failure, the promise will be rejected with the error code.
53
+ * In case of success, the promise will return the data object.
54
+ *
55
+ * @param path The API path from the API endpoint
56
+ * @param method The HTTP method to use (GET or POST)
57
+ * @param params The params to use for this request
58
+ * @returns {Promise<ApiGenericDataType>}
59
+ */
60
+export async function apiRequest(
61
+  path: string,
62
+  method: string,
63
+  params?: {...},
64
+): Promise<ApiGenericDataType> {
65
+  return new Promise(
66
+    (
67
+      resolve: (data: ApiGenericDataType) => void,
68
+      reject: (error: number) => void,
69
+    ) => {
70
+      let requestParams = {};
71
+      if (params != null) requestParams = {...params};
72
+      fetch(API_ENDPOINT + path, {
73
+        method,
74
+        headers: new Headers({
75
+          Accept: 'application/json',
76
+          'Content-Type': 'application/json',
77
+        }),
78
+        body: JSON.stringify(requestParams),
79
+      })
80
+        .then(async (response: Response): Promise<ApiResponseType> =>
81
+          response.json(),
82
+        )
83
+        .then((response: ApiResponseType) => {
84
+          if (isApiResponseValid(response)) {
85
+            if (response.error === ERROR_TYPE.SUCCESS) resolve(response.data);
86
+            else reject(response.error);
87
+          } else reject(ERROR_TYPE.SERVER_ERROR);
88
+        })
89
+        .catch((): void => reject(ERROR_TYPE.CONNECTION_ERROR));
90
+    },
91
+  );
82 92
 }
83 93
 
84 94
 /**
@@ -90,17 +100,15 @@ export function isResponseValid(response: response_format) {
90 100
  * If no data was found, returns an empty object
91 101
  *
92 102
  * @param url The urls to fetch data from
93
- * @return {Promise<Object>}
103
+ * @return Promise<{...}>
94 104
  */
95
-export async function readData(url: string) {
96
-    return new Promise((resolve, reject) => {
97
-        fetch(url)
98
-            .then(async (response) => response.json())
99
-            .then((data) => {
100
-                resolve(data);
101
-            })
102
-            .catch(() => {
103
-                reject();
104
-            });
105
-    });
105
+export async function readData(url: string): Promise<{...}> {
106
+  return new Promise(
107
+    (resolve: (response: {...}) => void, reject: () => void) => {
108
+      fetch(url)
109
+        .then(async (response: Response): Promise<{...}> => response.json())
110
+        .then((data: {...}): void => resolve(data))
111
+        .catch((): void => reject());
112
+    },
113
+  );
106 114
 }

Loading…
Cancel
Save