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
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<ApiGenericDataType | null>) => 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<Props, State> {
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
static defaultProps = {
errorViewOverride: null,
};
state = {
loading: true,
currentUserToken: string | null;
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;
errors: Array<number>;
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 (
<ErrorView
icon={override.icon}
message={override.message}
showRetryButton={override.showRetryButton}
/>
);
}
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
}
/**
* 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 (
<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())
);
}
render(): React.Node {
const {state, props} = this;
if (state.loading) return <BasicLoadingScreen />;
if (this.getError() === ERROR_TYPE.SUCCESS)
return props.renderFunction(this.fetchedData);
return this.getErrorRender();
}
}
export default AuthenticatedScreen;

View file

@ -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<R>}
* @returns {Promise<ApiGenericDataType>}
*/
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<ApiGenericDataType> {
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<ApiResponseType> =>
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<Object>}
* @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());
},
);
}