forked from vergnet/application-amicale
Improve requests handlers to match linter
This commit is contained in:
parent
9fc02baf6d
commit
0a9e0eb0ca
2 changed files with 268 additions and 274 deletions
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue