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,219 +1,205 @@
// @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 = {
errorViewOverride: null,
};
state = { currentUserToken: string | null;
loading: true,
connectionManager: ConnectionManager;
errors: Array<number>;
fetchedData: Array<ApiGenericDataType | null>;
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; * Refreshes screen if user changed
errors: Array<number>; */
fetchedData: Array<{ [key: string]: any } | null>; onScreenFocus = () => {
if (this.currentUserToken !== this.connectionManager.getToken()) {
this.currentUserToken = this.connectionManager.getToken();
this.fetchData();
}
};
constructor(props: Object) { /**
super(props); * Callback used when a request finishes, successfully or not.
this.connectionManager = ConnectionManager.getInstance(); * Saves data and error code.
this.props.navigation.addListener('focus', this.onScreenFocus); * If the token is invalid, logout the user and open the login screen.
this.fetchedData = new Array(this.props.requests.length); * If the last request was received, stop the loading screen.
this.errors = new Array(this.props.requests.length); *
* @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;
}
}
} }
/** if (shouldOverride && override != null) {
* Refreshes screen if user changed return (
*/ <ErrorView
onScreenFocus = () => { icon={override.icon}
if (this.currentUserToken !== this.connectionManager.getToken()) { message={override.message}
this.currentUserToken = this.connectionManager.getToken(); showRetryButton={override.showRetryButton}
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});
} }
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
}
/** /**
* Checks if all requests finished processing * Fetches the data from the server.
* *
* @return {boolean} True if all finished * If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
*/ *
allRequestsFinished() { * If the user is logged in, send all requests.
let finished = true; */
for (let i = 0; i < this.fetchedData.length; i++) { fetchData = () => {
if (this.fetchedData[i] === undefined) { const {state, props} = this;
finished = false; if (!state.loading) this.setState({loading: true});
break;
} if (this.connectionManager.isLoggedIn()) {
} for (let i = 0; i < props.requests.length; i += 1) {
return finished; 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. * Checks if all requests finished processing
* 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
* */
* @return {boolean} True if all finished successfully allRequestsFinished(): boolean {
*/ let finished = true;
allRequestsValid() { this.errors.forEach((error: number | null) => {
let valid = true; if (error == null) finished = false;
for (let i = 0; i < this.fetchedData.length; i++) { });
if (this.fetchedData[i] === null && this.props.requests[i].mandatory) { return finished;
valid = false; }
break;
}
}
return valid;
}
/** /**
* Gets the error to render. * Reloads the data, to be called using ref by parent components
* Non-mandatory requests are ignored. */
* reload() {
* this.fetchData();
* @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;
}
/** render(): React.Node {
* Gets the error view to display in case of error const {state, props} = this;
* if (state.loading) return <BasicLoadingScreen />;
* @return {*} if (this.getError() === ERROR_TYPE.SUCCESS)
*/ return props.renderFunction(this.fetchedData);
getErrorRender() { return this.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 (
<ErrorView
{...this.props}
icon={override.icon}
message={override.message}
showRetryButton={override.showRetryButton}
/>
);
} else {
return (
<ErrorView
{...this.props}
errorCode={errorCode}
onRefresh={this.fetchData}
/>
);
}
}
/**
* Reloads the data, to be called using ref by parent components
*/
reload() {
this.fetchData();
}
render() {
return (
this.state.loading
? <BasicLoadingScreen/>
: (this.allRequestsValid()
? this.props.renderFunction(this.fetchedData)
: this.getErrorRender())
);
}
} }
export default AuthenticatedScreen; export default AuthenticatedScreen;

View file

@ -1,25 +1,50 @@
// @flow // @flow
export const ERROR_TYPE = { export const ERROR_TYPE = {
SUCCESS: 0, SUCCESS: 0,
BAD_CREDENTIALS: 1, BAD_CREDENTIALS: 1,
BAD_TOKEN: 2, BAD_TOKEN: 2,
NO_CONSENT: 3, NO_CONSENT: 3,
TOKEN_SAVE: 4, TOKEN_SAVE: 4,
TOKEN_RETRIEVE: 5, TOKEN_RETRIEVE: 5,
BAD_INPUT: 400, BAD_INPUT: 400,
FORBIDDEN: 403, FORBIDDEN: 403,
CONNECTION_ERROR: 404, CONNECTION_ERROR: 404,
SERVER_ERROR: 500, SERVER_ERROR: 500,
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> {
fetch(API_ENDPOINT + path, { return new Promise(
method: method, (
headers: new Headers({ resolve: (data: ApiGenericDataType) => void,
'Accept': 'application/json', reject: (error: number) => void,
'Content-Type': 'application/json', ) => {
}), let requestParams = {};
body: JSON.stringify({ if (params != null) requestParams = {...params};
...params fetch(API_ENDPOINT + path, {
}) method,
}).then(async (response) => response.json()) headers: new Headers({
.then((response: response_format) => { Accept: 'application/json',
if (isResponseValid(response)) { 'Content-Type': 'application/json',
if (response.error === ERROR_TYPE.SUCCESS) }),
resolve(response.data); body: JSON.stringify(requestParams),
else })
reject(response.error); .then(async (response: Response): Promise<ApiResponseType> =>
} else response.json(),
reject(ERROR_TYPE.SERVER_ERROR); )
}) .then((response: ApiResponseType) => {
.catch(() => { if (isApiResponseValid(response)) {
reject(ERROR_TYPE.CONNECTION_ERROR); 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));
/** },
* 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(
fetch(url) (resolve: (response: {...}) => void, reject: () => void) => {
.then(async (response) => response.json()) fetch(url)
.then((data) => { .then(async (response: Response): Promise<{...}> => response.json())
resolve(data); .then((data: {...}): void => resolve(data))
}) .catch((): void => reject());
.catch(() => { },
reject(); );
});
});
} }