From 0a9e0eb0ca7b903139d5c22caa64f04c3d2aa155 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Sun, 2 Aug 2020 19:45:19 +0200 Subject: [PATCH] Improve requests handlers to match linter --- src/components/Amicale/AuthenticatedScreen.js | 382 +++++++++--------- src/utils/WebData.js | 160 ++++---- 2 files changed, 268 insertions(+), 274 deletions(-) diff --git a/src/components/Amicale/AuthenticatedScreen.js b/src/components/Amicale/AuthenticatedScreen.js index ac7ddc9..749ae16 100644 --- a/src/components/Amicale/AuthenticatedScreen.js +++ b/src/components/Amicale/AuthenticatedScreen.js @@ -1,219 +1,205 @@ // @flow import * as React from 'react'; -import ConnectionManager from "../../managers/ConnectionManager"; -import {ERROR_TYPE} from "../../utils/WebData"; -import ErrorView from "../Screens/ErrorView"; -import BasicLoadingScreen from "../Screens/BasicLoadingScreen"; -import {StackNavigationProp} from "@react-navigation/stack"; +import {StackNavigationProp} from '@react-navigation/stack'; +import ConnectionManager from '../../managers/ConnectionManager'; +import type {ApiGenericDataType} from '../../utils/WebData'; +import {ERROR_TYPE} from '../../utils/WebData'; +import ErrorView from '../Screens/ErrorView'; +import BasicLoadingScreen from '../Screens/BasicLoadingScreen'; -type Props = { - navigation: StackNavigationProp, - requests: Array<{ - link: string, - params: Object, - mandatory: boolean - }>, - renderFunction: (Array<{ [key: string]: any } | null>) => React.Node, - errorViewOverride?: Array<{ - errorCode: number, - message: string, - icon: string, - showRetryButton: boolean - }>, -} +type PropsType = { + navigation: StackNavigationProp, + requests: Array<{ + link: string, + params: {...}, + mandatory: boolean, + }>, + renderFunction: (Array) => React.Node, + errorViewOverride?: Array<{ + errorCode: number, + message: string, + icon: string, + showRetryButton: boolean, + }> | null, +}; -type State = { - loading: boolean, -} +type StateType = { + loading: boolean, +}; -class AuthenticatedScreen extends React.Component { +class AuthenticatedScreen extends React.Component { + static defaultProps = { + errorViewOverride: null, + }; - state = { - loading: true, + currentUserToken: string | null; + + connectionManager: ConnectionManager; + + errors: Array; + + fetchedData: Array; + + constructor(props: PropsType) { + super(props); + this.state = { + loading: true, }; + this.connectionManager = ConnectionManager.getInstance(); + props.navigation.addListener('focus', this.onScreenFocus); + this.fetchedData = new Array(props.requests.length); + this.errors = new Array(props.requests.length); + } - currentUserToken: string | null; - connectionManager: ConnectionManager; - errors: Array; - fetchedData: Array<{ [key: string]: any } | null>; + /** + * Refreshes screen if user changed + */ + onScreenFocus = () => { + if (this.currentUserToken !== this.connectionManager.getToken()) { + this.currentUserToken = this.connectionManager.getToken(); + this.fetchData(); + } + }; - constructor(props: Object) { - super(props); - this.connectionManager = ConnectionManager.getInstance(); - this.props.navigation.addListener('focus', this.onScreenFocus); - this.fetchedData = new Array(this.props.requests.length); - this.errors = new Array(this.props.requests.length); + /** + * Callback used when a request finishes, successfully or not. + * Saves data and error code. + * If the token is invalid, logout the user and open the login screen. + * If the last request was received, stop the loading screen. + * + * @param data The data fetched from the server + * @param index The index for the data + * @param error The error code received + */ + onRequestFinished( + data: ApiGenericDataType | null, + index: number, + error?: number, + ) { + const {props} = this; + if (index >= 0 && index < props.requests.length) { + this.fetchedData[index] = data; + this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS; + } + // Token expired, logout user + if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect(); + + if (this.allRequestsFinished()) this.setState({loading: false}); + } + + /** + * Gets the error to render. + * Non-mandatory requests are ignored. + * + * + * @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found + */ + getError(): number { + const {props} = this; + for (let i = 0; i < this.errors.length; i += 1) { + if ( + this.errors[i] !== ERROR_TYPE.SUCCESS && + props.requests[i].mandatory + ) { + return this.errors[i]; + } + } + return ERROR_TYPE.SUCCESS; + } + + /** + * Gets the error view to display in case of error + * + * @return {*} + */ + getErrorRender(): React.Node { + const {props} = this; + const errorCode = this.getError(); + let shouldOverride = false; + let override = null; + const overrideList = props.errorViewOverride; + if (overrideList != null) { + for (let i = 0; i < overrideList.length; i += 1) { + if (overrideList[i].errorCode === errorCode) { + shouldOverride = true; + override = overrideList[i]; + break; + } + } } - /** - * Refreshes screen if user changed - */ - onScreenFocus = () => { - if (this.currentUserToken !== this.connectionManager.getToken()) { - this.currentUserToken = this.connectionManager.getToken(); - this.fetchData(); - } - }; - - /** - * Fetches the data from the server. - * - * If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail. - * - * If the user is logged in, send all requests. - */ - fetchData = () => { - if (!this.state.loading) - this.setState({loading: true}); - if (this.connectionManager.isLoggedIn()) { - for (let i = 0; i < this.props.requests.length; i++) { - this.connectionManager.authenticatedRequest( - this.props.requests[i].link, - this.props.requests[i].params) - .then((data) => { - this.onRequestFinished(data, i, -1); - }) - .catch((error) => { - this.onRequestFinished(null, i, error); - }); - } - } else { - for (let i = 0; i < this.props.requests.length; i++) { - this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN); - } - } - }; - - /** - * Callback used when a request finishes, successfully or not. - * Saves data and error code. - * If the token is invalid, logout the user and open the login screen. - * If the last request was received, stop the loading screen. - * - * @param data The data fetched from the server - * @param index The index for the data - * @param error The error code received - */ - onRequestFinished(data: { [key: string]: any } | null, index: number, error: number) { - if (index >= 0 && index < this.props.requests.length) { - this.fetchedData[index] = data; - this.errors[index] = error; - } - - if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user - this.connectionManager.disconnect(); - - if (this.allRequestsFinished()) - this.setState({loading: false}); + if (shouldOverride && override != null) { + return ( + + ); } + return ; + } - /** - * Checks if all requests finished processing - * - * @return {boolean} True if all finished - */ - allRequestsFinished() { - let finished = true; - for (let i = 0; i < this.fetchedData.length; i++) { - if (this.fetchedData[i] === undefined) { - finished = false; - break; - } - } - return finished; + /** + * Fetches the data from the server. + * + * If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail. + * + * If the user is logged in, send all requests. + */ + fetchData = () => { + const {state, props} = this; + if (!state.loading) this.setState({loading: true}); + + if (this.connectionManager.isLoggedIn()) { + for (let i = 0; i < props.requests.length; i += 1) { + this.connectionManager + .authenticatedRequest( + props.requests[i].link, + props.requests[i].params, + ) + .then((response: ApiGenericDataType): void => + this.onRequestFinished(response, i), + ) + .catch((error: number): void => + this.onRequestFinished(null, i, error), + ); + } + } else { + for (let i = 0; i < props.requests.length; i += 1) { + this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN); + } } + }; - /** - * Checks if all requests have finished successfully. - * This will return false only if a mandatory request failed. - * All non-mandatory requests can fail without impacting the return value. - * - * @return {boolean} True if all finished successfully - */ - allRequestsValid() { - let valid = true; - for (let i = 0; i < this.fetchedData.length; i++) { - if (this.fetchedData[i] === null && this.props.requests[i].mandatory) { - valid = false; - break; - } - } - return valid; - } + /** + * Checks if all requests finished processing + * + * @return {boolean} True if all finished + */ + allRequestsFinished(): boolean { + let finished = true; + this.errors.forEach((error: number | null) => { + if (error == null) finished = false; + }); + return finished; + } - /** - * Gets the error to render. - * Non-mandatory requests are ignored. - * - * - * @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found - */ - getError() { - for (let i = 0; i < this.errors.length; i++) { - if (this.errors[i] !== 0 && this.props.requests[i].mandatory) { - return this.errors[i]; - } - } - return ERROR_TYPE.SUCCESS; - } + /** + * Reloads the data, to be called using ref by parent components + */ + reload() { + this.fetchData(); + } - /** - * Gets the error view to display in case of error - * - * @return {*} - */ - getErrorRender() { - const errorCode = this.getError(); - let shouldOverride = false; - let override = null; - const overrideList = this.props.errorViewOverride; - if (overrideList != null) { - for (let i = 0; i < overrideList.length; i++) { - if (overrideList[i].errorCode === errorCode) { - shouldOverride = true; - override = overrideList[i]; - break; - } - } - } - if (shouldOverride && override != null) { - return ( - - ); - } else { - return ( - - ); - } - - } - - /** - * Reloads the data, to be called using ref by parent components - */ - reload() { - this.fetchData(); - } - - render() { - return ( - this.state.loading - ? - : (this.allRequestsValid() - ? this.props.renderFunction(this.fetchedData) - : this.getErrorRender()) - ); - } + render(): React.Node { + const {state, props} = this; + if (state.loading) return ; + if (this.getError() === ERROR_TYPE.SUCCESS) + return props.renderFunction(this.fetchedData); + return this.getErrorRender(); + } } export default AuthenticatedScreen; diff --git a/src/utils/WebData.js b/src/utils/WebData.js index 03bef6f..2c9b6ee 100644 --- a/src/utils/WebData.js +++ b/src/utils/WebData.js @@ -1,25 +1,50 @@ // @flow export const ERROR_TYPE = { - SUCCESS: 0, - BAD_CREDENTIALS: 1, - BAD_TOKEN: 2, - NO_CONSENT: 3, - TOKEN_SAVE: 4, - TOKEN_RETRIEVE: 5, - BAD_INPUT: 400, - FORBIDDEN: 403, - CONNECTION_ERROR: 404, - SERVER_ERROR: 500, - UNKNOWN: 999, + SUCCESS: 0, + BAD_CREDENTIALS: 1, + BAD_TOKEN: 2, + NO_CONSENT: 3, + TOKEN_SAVE: 4, + TOKEN_RETRIEVE: 5, + BAD_INPUT: 400, + FORBIDDEN: 403, + CONNECTION_ERROR: 404, + SERVER_ERROR: 500, + UNKNOWN: 999, }; -type response_format = { - error: number, - data: Object, -} +export type ApiDataLoginType = { + token: string, +}; -const API_ENDPOINT = "https://www.amicale-insat.fr/api/"; +// eslint-disable-next-line flowtype/no-weak-types +export type ApiGenericDataType = {[key: string]: any}; + +type ApiResponseType = { + error: number, + data: ApiGenericDataType, +}; + +const API_ENDPOINT = 'https://www.amicale-insat.fr/api/'; + +/** + * Checks if the given API response is valid. + * + * For a request to be valid, it must match the response_format as defined in this file. + * + * @param response + * @returns {boolean} + */ +export function isApiResponseValid(response: ApiResponseType): boolean { + return ( + response != null && + response.error != null && + typeof response.error === 'number' && + response.data != null && + typeof response.data === 'object' + ); +} /** * Sends a request to the Amicale Website backend @@ -30,55 +55,40 @@ const API_ENDPOINT = "https://www.amicale-insat.fr/api/"; * @param path The API path from the API endpoint * @param method The HTTP method to use (GET or POST) * @param params The params to use for this request - * @returns {Promise} + * @returns {Promise} */ -export async function apiRequest(path: string, method: string, params: ?{ [key: string]: string | number }) { - if (params === undefined || params === null) - params = {}; - - return new Promise((resolve, reject) => { - fetch(API_ENDPOINT + path, { - method: method, - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - ...params - }) - }).then(async (response) => response.json()) - .then((response: response_format) => { - if (isResponseValid(response)) { - if (response.error === ERROR_TYPE.SUCCESS) - resolve(response.data); - else - reject(response.error); - } else - reject(ERROR_TYPE.SERVER_ERROR); - }) - .catch(() => { - reject(ERROR_TYPE.CONNECTION_ERROR); - }); - }); -} - -/** - * Checks if the given API response is valid. - * - * For a request to be valid, it must match the response_format as defined in this file. - * - * @param response - * @returns {boolean} - */ -export function isResponseValid(response: response_format) { - let valid = response !== undefined - && response.error !== undefined - && typeof response.error === "number"; - - valid = valid - && response.data !== undefined - && typeof response.data === "object"; - return valid; +export async function apiRequest( + path: string, + method: string, + params?: {...}, +): Promise { + return new Promise( + ( + resolve: (data: ApiGenericDataType) => void, + reject: (error: number) => void, + ) => { + let requestParams = {}; + if (params != null) requestParams = {...params}; + fetch(API_ENDPOINT + path, { + method, + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(requestParams), + }) + .then(async (response: Response): Promise => + response.json(), + ) + .then((response: ApiResponseType) => { + if (isApiResponseValid(response)) { + if (response.error === ERROR_TYPE.SUCCESS) resolve(response.data); + else reject(response.error); + } else reject(ERROR_TYPE.SERVER_ERROR); + }) + .catch((): void => reject(ERROR_TYPE.CONNECTION_ERROR)); + }, + ); } /** @@ -90,17 +100,15 @@ export function isResponseValid(response: response_format) { * If no data was found, returns an empty object * * @param url The urls to fetch data from - * @return {Promise} + * @return Promise<{...}> */ -export async function readData(url: string) { - return new Promise((resolve, reject) => { - fetch(url) - .then(async (response) => response.json()) - .then((data) => { - resolve(data); - }) - .catch(() => { - reject(); - }); - }); +export async function readData(url: string): Promise<{...}> { + return new Promise( + (resolve: (response: {...}) => void, reject: () => void) => { + fetch(url) + .then(async (response: Response): Promise<{...}> => response.json()) + .then((data: {...}): void => resolve(data)) + .catch((): void => reject()); + }, + ); }