Improve requests handlers to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-02 19:45:19 +02:00
parent 9fc02baf6d
commit 0a9e0eb0ca
2 changed files with 268 additions and 274 deletions

View file

@ -1,49 +1,55 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import ConnectionManager from "../../managers/ConnectionManager"; import {StackNavigationProp} from '@react-navigation/stack';
import {ERROR_TYPE} from "../../utils/WebData"; import ConnectionManager from '../../managers/ConnectionManager';
import ErrorView from "../Screens/ErrorView"; import type {ApiGenericDataType} from '../../utils/WebData';
import BasicLoadingScreen from "../Screens/BasicLoadingScreen"; import {ERROR_TYPE} from '../../utils/WebData';
import {StackNavigationProp} from "@react-navigation/stack"; import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
requests: Array<{ requests: Array<{
link: string, link: string,
params: Object, params: {...},
mandatory: boolean mandatory: boolean,
}>, }>,
renderFunction: (Array<{ [key: string]: any } | null>) => React.Node, renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
errorViewOverride?: Array<{ errorViewOverride?: Array<{
errorCode: number, errorCode: number,
message: string, message: string,
icon: string, icon: string,
showRetryButton: boolean showRetryButton: boolean,
}>, }> | null,
} };
type State = { type StateType = {
loading: boolean, loading: boolean,
} };
class AuthenticatedScreen extends React.Component<Props, State> { class AuthenticatedScreen extends React.Component<PropsType, StateType> {
static defaultProps = {
state = { errorViewOverride: null,
loading: true,
}; };
currentUserToken: string | null; currentUserToken: string | null;
connectionManager: ConnectionManager;
errors: Array<number>;
fetchedData: Array<{ [key: string]: any } | null>;
constructor(props: Object) { connectionManager: ConnectionManager;
errors: Array<number>;
fetchedData: Array<ApiGenericDataType | null>;
constructor(props: PropsType) {
super(props); super(props);
this.state = {
loading: true,
};
this.connectionManager = ConnectionManager.getInstance(); this.connectionManager = ConnectionManager.getInstance();
this.props.navigation.addListener('focus', this.onScreenFocus); props.navigation.addListener('focus', this.onScreenFocus);
this.fetchedData = new Array(this.props.requests.length); this.fetchedData = new Array(props.requests.length);
this.errors = new Array(this.props.requests.length); this.errors = new Array(props.requests.length);
} }
/** /**
@ -56,35 +62,6 @@ class AuthenticatedScreen extends React.Component<Props, State> {
} }
}; };
/**
* 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. * Callback used when a request finishes, successfully or not.
* Saves data and error code. * Saves data and error code.
@ -95,51 +72,20 @@ class AuthenticatedScreen extends React.Component<Props, State> {
* @param index The index for the data * @param index The index for the data
* @param error The error code received * @param error The error code received
*/ */
onRequestFinished(data: { [key: string]: any } | null, index: number, error: number) { onRequestFinished(
if (index >= 0 && index < this.props.requests.length) { data: ApiGenericDataType | null,
index: number,
error?: number,
) {
const {props} = this;
if (index >= 0 && index < props.requests.length) {
this.fetchedData[index] = data; this.fetchedData[index] = data;
this.errors[index] = error; this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
} }
// Token expired, logout user
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect();
if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user if (this.allRequestsFinished()) this.setState({loading: false});
this.connectionManager.disconnect();
if (this.allRequestsFinished())
this.setState({loading: false});
}
/**
* 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;
}
/**
* 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;
} }
/** /**
@ -149,9 +95,13 @@ class AuthenticatedScreen extends React.Component<Props, State> {
* *
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found * @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
*/ */
getError() { getError(): number {
for (let i = 0; i < this.errors.length; i++) { const {props} = this;
if (this.errors[i] !== 0 && this.props.requests[i].mandatory) { 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 this.errors[i];
} }
} }
@ -163,13 +113,14 @@ class AuthenticatedScreen extends React.Component<Props, State> {
* *
* @return {*} * @return {*}
*/ */
getErrorRender() { getErrorRender(): React.Node {
const {props} = this;
const errorCode = this.getError(); const errorCode = this.getError();
let shouldOverride = false; let shouldOverride = false;
let override = null; let override = null;
const overrideList = this.props.errorViewOverride; const overrideList = props.errorViewOverride;
if (overrideList != null) { if (overrideList != null) {
for (let i = 0; i < overrideList.length; i++) { for (let i = 0; i < overrideList.length; i += 1) {
if (overrideList[i].errorCode === errorCode) { if (overrideList[i].errorCode === errorCode) {
shouldOverride = true; shouldOverride = true;
override = overrideList[i]; override = overrideList[i];
@ -177,25 +128,62 @@ class AuthenticatedScreen extends React.Component<Props, State> {
} }
} }
} }
if (shouldOverride && override != null) { if (shouldOverride && override != null) {
return ( return (
<ErrorView <ErrorView
{...this.props}
icon={override.icon} icon={override.icon}
message={override.message} message={override.message}
showRetryButton={override.showRetryButton} showRetryButton={override.showRetryButton}
/> />
); );
} else { }
return ( return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
<ErrorView
{...this.props}
errorCode={errorCode}
onRefresh={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 = () => {
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 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;
} }
/** /**
@ -205,14 +193,12 @@ class AuthenticatedScreen extends React.Component<Props, State> {
this.fetchData(); this.fetchData();
} }
render() { render(): React.Node {
return ( const {state, props} = this;
this.state.loading if (state.loading) return <BasicLoadingScreen />;
? <BasicLoadingScreen/> if (this.getError() === ERROR_TYPE.SUCCESS)
: (this.allRequestsValid() return props.renderFunction(this.fetchedData);
? this.props.renderFunction(this.fetchedData) return this.getErrorRender();
: this.getErrorRender())
);
} }
} }

View file

@ -14,12 +14,37 @@ export const ERROR_TYPE = {
UNKNOWN: 999, UNKNOWN: 999,
}; };
type response_format = { export type ApiDataLoginType = {
error: number, token: string,
data: Object, };
}
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 * 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 path The API path from the API endpoint
* @param method The HTTP method to use (GET or POST) * @param method The HTTP method to use (GET or POST)
* @param params The params to use for this request * @param params The params to use for this request
* @returns {Promise<R>} * @returns {Promise<ApiGenericDataType>}
*/ */
export async function apiRequest(path: string, method: string, params: ?{ [key: string]: string | number }) { export async function apiRequest(
if (params === undefined || params === null) path: string,
params = {}; method: string,
params?: {...},
return new Promise((resolve, reject) => { ): Promise<ApiGenericDataType> {
return new Promise(
(
resolve: (data: ApiGenericDataType) => void,
reject: (error: number) => void,
) => {
let requestParams = {};
if (params != null) requestParams = {...params};
fetch(API_ENDPOINT + path, { fetch(API_ENDPOINT + path, {
method: method, method,
headers: new Headers({ headers: new Headers({
'Accept': 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}), }),
body: JSON.stringify({ body: JSON.stringify(requestParams),
...params
}) })
}).then(async (response) => response.json()) .then(async (response: Response): Promise<ApiResponseType> =>
.then((response: response_format) => { response.json(),
if (isResponseValid(response)) { )
if (response.error === ERROR_TYPE.SUCCESS) .then((response: ApiResponseType) => {
resolve(response.data); if (isApiResponseValid(response)) {
else if (response.error === ERROR_TYPE.SUCCESS) resolve(response.data);
reject(response.error); else reject(response.error);
} else } else reject(ERROR_TYPE.SERVER_ERROR);
reject(ERROR_TYPE.SERVER_ERROR);
}) })
.catch(() => { .catch((): void => reject(ERROR_TYPE.CONNECTION_ERROR));
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;
} }
/** /**
@ -90,17 +100,15 @@ export function isResponseValid(response: response_format) {
* If no data was found, returns an empty object * If no data was found, returns an empty object
* *
* @param url The urls to fetch data from * @param url The urls to fetch data from
* @return {Promise<Object>} * @return Promise<{...}>
*/ */
export async function readData(url: string) { export async function readData(url: string): Promise<{...}> {
return new Promise((resolve, reject) => { return new Promise(
(resolve: (response: {...}) => void, reject: () => void) => {
fetch(url) fetch(url)
.then(async (response) => response.json()) .then(async (response: Response): Promise<{...}> => response.json())
.then((data) => { .then((data: {...}): void => resolve(data))
resolve(data); .catch((): void => reject());
}) },
.catch(() => { );
reject();
});
});
} }