From 0ba48adb6e786abddc9ad218c657e3c7ff22023a Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Thu, 9 Apr 2020 15:59:54 +0200 Subject: [PATCH] Improved error handling and added new api endpoints --- __tests__/managers/ConnectionManager.test.js | 44 +------ __tests__/utils/WebData.js | 45 +++++++ src/components/Amicale/AuthenticatedScreen.js | 29 +++-- src/components/Amicale/Vote/VoteSelect.js | 3 +- src/components/Custom/ErrorView.js | 2 +- src/components/Lists/WebSectionList.js | 3 +- src/managers/ConnectionManager.js | 122 ++++-------------- .../Amicale/Clubs/ClubDisplayScreen.js | 32 +---- src/screens/Amicale/Clubs/ClubListScreen.js | 3 +- src/screens/Amicale/ProfileScreen.js | 16 +-- src/screens/Amicale/VoteScreen.js | 4 +- src/screens/Planning/PlanningDisplayScreen.js | 67 +++++++--- src/utils/WebData.js | 60 +++++++++ 13 files changed, 219 insertions(+), 211 deletions(-) create mode 100644 __tests__/utils/WebData.js diff --git a/__tests__/managers/ConnectionManager.test.js b/__tests__/managers/ConnectionManager.test.js index 0e3d9e7..2a9a996 100644 --- a/__tests__/managers/ConnectionManager.test.js +++ b/__tests__/managers/ConnectionManager.test.js @@ -1,5 +1,6 @@ import React from 'react'; -import ConnectionManager, {ERROR_TYPE} from "../../src/managers/ConnectionManager"; +import ConnectionManager from "../../src/managers/ConnectionManager"; +import {ERROR_TYPE} from "../../src/utils/WebData"; import * as SecureStore from 'expo-secure-store'; let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native @@ -63,47 +64,6 @@ test('recoverLogin success saved', () => { return expect(c.recoverLogin()).resolves.toBe('token2'); }); -test('isRequestResponseValid', () => { - let json = { - error: 0, - data: {} - }; - expect(c.isResponseValid(json)).toBeTrue(); - json = { - error: 1, - data: {} - }; - expect(c.isResponseValid(json)).toBeTrue(); - json = { - error: 50, - data: {} - }; - expect(c.isResponseValid(json)).toBeTrue(); - json = { - error: 50, - data: {truc: 'machin'} - }; - expect(c.isResponseValid(json)).toBeTrue(); - json = { - message: 'coucou' - }; - expect(c.isResponseValid(json)).toBeFalse(); - json = { - error: 'coucou', - data: {truc: 'machin'} - }; - expect(c.isResponseValid(json)).toBeFalse(); - json = { - error: 0, - data: 'coucou' - }; - expect(c.isResponseValid(json)).toBeFalse(); - json = { - error: 0, - }; - expect(c.isResponseValid(json)).toBeFalse(); -}); - test("isConnectionResponseValid", () => { let json = { error: 0, diff --git a/__tests__/utils/WebData.js b/__tests__/utils/WebData.js new file mode 100644 index 0000000..9f8fb5b --- /dev/null +++ b/__tests__/utils/WebData.js @@ -0,0 +1,45 @@ +import React from 'react'; +import {isResponseValid} from "../../src/utils/WebData"; + +let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native + +test('isRequestResponseValid', () => { + let json = { + error: 0, + data: {} + }; + expect(isResponseValid(json)).toBeTrue(); + json = { + error: 1, + data: {} + }; + expect(isResponseValid(json)).toBeTrue(); + json = { + error: 50, + data: {} + }; + expect(isResponseValid(json)).toBeTrue(); + json = { + error: 50, + data: {truc: 'machin'} + }; + expect(isResponseValid(json)).toBeTrue(); + json = { + message: 'coucou' + }; + expect(isResponseValid(json)).toBeFalse(); + json = { + error: 'coucou', + data: {truc: 'machin'} + }; + expect(isResponseValid(json)).toBeFalse(); + json = { + error: 0, + data: 'coucou' + }; + expect(isResponseValid(json)).toBeFalse(); + json = { + error: 0, + }; + expect(isResponseValid(json)).toBeFalse(); +}); diff --git a/src/components/Amicale/AuthenticatedScreen.js b/src/components/Amicale/AuthenticatedScreen.js index 940336d..b2ca001 100644 --- a/src/components/Amicale/AuthenticatedScreen.js +++ b/src/components/Amicale/AuthenticatedScreen.js @@ -1,13 +1,18 @@ // @flow import * as React from 'react'; -import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager"; +import ConnectionManager from "../../managers/ConnectionManager"; +import {ERROR_TYPE} from "../../utils/WebData"; import ErrorView from "../Custom/ErrorView"; import BasicLoadingScreen from "../Custom/BasicLoadingScreen"; type Props = { navigation: Object, - links: Array<{link: string, mandatory: boolean}>, + requests: Array<{ + link: string, + params: Object, + mandatory: boolean + }>, renderFunction: Function, } @@ -30,15 +35,15 @@ class AuthenticatedScreen extends React.Component { super(props); this.connectionManager = ConnectionManager.getInstance(); this.props.navigation.addListener('focus', this.onScreenFocus); - this.fetchedData = new Array(this.props.links.length); - this.errors = new Array(this.props.links.length); + this.fetchedData = new Array(this.props.requests.length); + this.errors = new Array(this.props.requests.length); } /** * Refreshes screen if user changed */ onScreenFocus = () => { - if (this.currentUserToken !== this.connectionManager.getToken()){ + if (this.currentUserToken !== this.connectionManager.getToken()) { this.currentUserToken = this.connectionManager.getToken(); this.fetchData(); } @@ -55,8 +60,10 @@ class AuthenticatedScreen extends React.Component { if (!this.state.loading) this.setState({loading: true}); if (this.connectionManager.isLoggedIn()) { - for (let i = 0; i < this.props.links.length; i++) { - this.connectionManager.authenticatedRequest(this.props.links[i].link, null, null) + 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); }) @@ -65,7 +72,7 @@ class AuthenticatedScreen extends React.Component { }); } } else { - for (let i = 0; i < this.props.links.length; i++) { + for (let i = 0; i < this.props.requests.length; i++) { this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN); } } @@ -82,7 +89,7 @@ class AuthenticatedScreen extends React.Component { * @param error The error code received */ onRequestFinished(data: Object | null, index: number, error: number) { - if (index >= 0 && index < this.props.links.length){ + if (index >= 0 && index < this.props.requests.length) { this.fetchedData[index] = data; this.errors[index] = error; } @@ -120,7 +127,7 @@ class AuthenticatedScreen extends React.Component { allRequestsValid() { let valid = true; for (let i = 0; i < this.fetchedData.length; i++) { - if (this.fetchedData[i] === null && this.props.links[i].mandatory) { + if (this.fetchedData[i] === null && this.props.requests[i].mandatory) { valid = false; break; } @@ -137,7 +144,7 @@ class AuthenticatedScreen extends React.Component { */ getError() { for (let i = 0; i < this.errors.length; i++) { - if (this.errors[i] !== 0 && this.props.links[i].mandatory) { + if (this.errors[i] !== 0 && this.props.requests[i].mandatory) { return this.errors[i]; } } diff --git a/src/components/Amicale/Vote/VoteSelect.js b/src/components/Amicale/Vote/VoteSelect.js index 24b9e89..8d98a28 100644 --- a/src/components/Amicale/Vote/VoteSelect.js +++ b/src/components/Amicale/Vote/VoteSelect.js @@ -45,8 +45,7 @@ export default class VoteSelect extends React.PureComponent { return new Promise((resolve, reject) => { ConnectionManager.getInstance().authenticatedRequest( "elections/vote", - ["vote"], - [parseInt(this.state.selectedTeam)]) + {"vote": parseInt(this.state.selectedTeam)}) .then(() => { this.onVoteDialogDismiss(); this.props.onVoteSuccess(); diff --git a/src/components/Custom/ErrorView.js b/src/components/Custom/ErrorView.js index 8b1c117..0b617e2 100644 --- a/src/components/Custom/ErrorView.js +++ b/src/components/Custom/ErrorView.js @@ -5,7 +5,7 @@ import {Button, Subheading, withTheme} from 'react-native-paper'; import {StyleSheet, View} from "react-native"; import {MaterialCommunityIcons} from "@expo/vector-icons"; import i18n from 'i18n-js'; -import {ERROR_TYPE} from "../../managers/ConnectionManager"; +import {ERROR_TYPE} from "../../utils/WebData"; type Props = { navigation: Object, diff --git a/src/components/Lists/WebSectionList.js b/src/components/Lists/WebSectionList.js index b46f44b..ce6249e 100644 --- a/src/components/Lists/WebSectionList.js +++ b/src/components/Lists/WebSectionList.js @@ -1,13 +1,12 @@ // @flow import * as React from 'react'; -import {readData} from "../../utils/WebData"; +import {ERROR_TYPE, readData} from "../../utils/WebData"; import i18n from "i18n-js"; import {Snackbar} from 'react-native-paper'; import {RefreshControl, SectionList, View} from "react-native"; import ErrorView from "../Custom/ErrorView"; import BasicLoadingScreen from "../Custom/BasicLoadingScreen"; -import {ERROR_TYPE} from "../../managers/ConnectionManager"; type Props = { navigation: Object, diff --git a/src/managers/ConnectionManager.js b/src/managers/ConnectionManager.js index 5bd7cb2..9ff1675 100644 --- a/src/managers/ConnectionManager.js +++ b/src/managers/ConnectionManager.js @@ -1,23 +1,7 @@ // @flow import * as SecureStore from 'expo-secure-store'; - -export const ERROR_TYPE = { - SUCCESS: 0, - BAD_CREDENTIALS: 1, - BAD_TOKEN: 2, - NO_CONSENT: 3, - BAD_INPUT: 400, - FORBIDDEN: 403, - CONNECTION_ERROR: 404, - SERVER_ERROR: 500, - UNKNOWN: 999, -}; - -type response_format = { - error: number, - data: Object, -} +import {apiRequest, ERROR_TYPE, isResponseValid} from "../utils/WebData"; /** * champ: error @@ -30,7 +14,7 @@ type response_format = { * 500 : SERVER_ERROR -> pb coté serveur */ -const API_ENDPOINT = "https://www.amicale-insat.fr/api/"; + const AUTH_PATH = "password"; export default class ConnectionManager { @@ -126,53 +110,27 @@ export default class ConnectionManager { } async connect(email: string, password: string) { - let data = { - email: email, - password: password, - }; return new Promise((resolve, reject) => { - fetch(API_ENDPOINT + AUTH_PATH, { - method: 'POST', - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify(data) - }).then(async (response) => response.json()) - .then((response: response_format) => { - if (this.isConnectionResponseValid(response)) { - if (response.error === ERROR_TYPE.SUCCESS) { - this.saveLogin(email, response.data.token) - .then(() => { - resolve(true); - }) - .catch(() => { - reject(ERROR_TYPE.UNKNOWN); - }); - } else - reject(response.error); - } else - reject(ERROR_TYPE.CONNECTION_ERROR); + const data = { + email: email, + password: password, + }; + apiRequest(AUTH_PATH, 'POST', data) + .then((response) => { + this.saveLogin(email, response.token) + .then(() => { + resolve(true); + }) + .catch(() => { + reject(ERROR_TYPE.UNKNOWN); + }); }) - .catch((error) => { - reject(ERROR_TYPE.CONNECTION_ERROR); - }); + .catch((error) => reject(error)); }); } - 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; - } - - isConnectionResponseValid(response: response_format) { - let valid = this.isResponseValid(response); + isConnectionResponseValid(response: Object) { + let valid = isResponseValid(response); if (valid && response.error === ERROR_TYPE.SUCCESS) valid = valid @@ -182,45 +140,17 @@ export default class ConnectionManager { return valid; } - generatePostArguments(keys: Array, values: Array) { - let data = {}; - for (let i = 0; i < keys.length; i++) { - data[keys[i]] = values[i]; - } - return data; - } - - async authenticatedRequest(path: string, keys: Array|null, values: Array|null) { + async authenticatedRequest(path: string, params: Object) { return new Promise((resolve, reject) => { if (this.getToken() !== null) { - let data = {}; - if (keys !== null && values !== null && keys.length === values.length) - data = this.generatePostArguments(keys, values); // console.log(data); - fetch(API_ENDPOINT + path, { - method: 'POST', - headers: new Headers({ - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }), - body: JSON.stringify({ - token: this.getToken(), - ...data - }) - }).then(async (response) => response.json()) - .then((response: response_format) => { - // console.log(response); - if (this.isResponseValid(response)) { - if (response.error === ERROR_TYPE.SUCCESS) - resolve(response.data); - else - reject(response.error); - } else - reject(ERROR_TYPE.CONNECTION_ERROR); - }) - .catch(() => { - reject(ERROR_TYPE.CONNECTION_ERROR); - }); + let data = { + token: this.getToken(), + ...params + }; + apiRequest(path, 'POST', data) + .then((response) => resolve(response)) + .catch((error) => reject(error)); } else reject(ERROR_TYPE.UNKNOWN); }); diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.js b/src/screens/Amicale/Clubs/ClubDisplayScreen.js index 9d0f026..2feafd0 100644 --- a/src/screens/Amicale/Clubs/ClubDisplayScreen.js +++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.js @@ -22,22 +22,6 @@ function openWebLink(event, link) { Linking.openURL(link).catch((err) => console.error('Error opening link', err)); } -const FakeClub = { - "category": [ - 3, - 6, - ], - "description": "

Les 100 Tours de l’INSA reviennent en force pour une cinquième édition les 5 et 6 juin prochain !


Prépare-toi pour le plus gros évènement de l’année sur le campus de notre belle école qui nous réunit tous autour d’activités folles pour fêter la fin de l’année dans la bonne humeur !

L’éco-festival tournera autour d’une grande course par équipe qui nous vaut ce doux nom de 100 tours. Ce sera le moment de défier tes potes pour tenter de remporter de nombreux lots, et surtout l’admiration de tous. Mais cela ne s’arrête pas là, puisque tu pourras aussi participer à des activités à sensation, divers ateliers, et de quoi chiller avec tes potes en écoutant de la bonne musique. Tu pourras ensuite enchaîner sur LA soirée de l’année, rythmée par des artistes sur-motivés !


Tu es bien entendu le bienvenu si l’envie te prend de rejoindre l’équipe et de nous aider à organiser cet évènement du turfu !


La team 100 Tours


Contact : 100tours@amicale-insat.fr

Facebook : Les 100 Tours de l’INSA

Instagram : 100tours.insatoulouse

", - "id": 110, - "logo": "https://www.amicale-insat.fr/storage/clubLogos/2cca8885dd3bdf902124f038b548962b.jpeg", - "name": "100 Tours", - "responsibles": [ - "Juliette Duval", - "Emilie Cuminal", - "Maxime Doré", - ], -}; - /** * Class defining a club event information page. * If called with data and categories navigation parameters, will use those to display the data. @@ -132,11 +116,8 @@ class ClubDisplayScreen extends React.Component { this.props.navigation.setOptions({title: data.name}) } - getScreen = (data: Object) => { - console.log('fetchedData passed to screen:'); - console.log(data); - console.log("Using fake data"); - data = FakeClub; + getScreen = (response: Array) => { + let data = response[0]; this.updateHeaderTitle(data); return ( @@ -182,16 +163,17 @@ class ClubDisplayScreen extends React.Component { if (this.shouldFetchData) return ; else - return this.getScreen(this.displayData); + return this.getScreen([this.displayData]); } } diff --git a/src/screens/Amicale/Clubs/ClubListScreen.js b/src/screens/Amicale/Clubs/ClubListScreen.js index 78a77bc..b3be179 100644 --- a/src/screens/Amicale/Clubs/ClubListScreen.js +++ b/src/screens/Amicale/Clubs/ClubListScreen.js @@ -193,9 +193,10 @@ class ClubListScreen extends React.Component { return ( { * @return {*} */ clubListItem = ({item}: Object) => { - const onPress = () => this.openClubDetailsScreen(0); // TODO get club id - const isManager = false; // TODO detect if manager + const onPress = () => this.openClubDetailsScreen(item.id); let description = i18n.t("profileScreen.isMember"); let icon = (props) => ; - if (isManager) { + if (item.is_manager) { description = i18n.t("profileScreen.isManager"); icon = (props) => ; } @@ -289,17 +288,13 @@ class ProfileScreen extends React.Component { * @param list The club list * @return {*} */ - getClubList(list: Array) { - let dataset = []; - for (let i = 0; i < list.length; i++) { - dataset.push({name: list[i]}); - } + getClubList(list: Array) { return ( //$FlowFixMe ); } @@ -308,9 +303,10 @@ class ProfileScreen extends React.Component { return ( { console.error('Error opening link', err)); } -const FAKE_EVENT = { - "id": 142, - "title": "Soir\u00e9e Impact'INSA", - "logo": null, - "date_begin": "2020-04-22 19:00", - "date_end": "2020-04-22 00:00", - "description": "

R\u00e9servation salle de boom + PK pour la soir\u00e9e Impact'Insa<\/p>", - "club": "Impact Insa", - "category_id": 10, - "url": "https:\/\/www.amicale-insat.fr\/event\/142\/view" -}; +const CLUB_INFO_PATH = "event/info"; /** * Class defining a planning event information page. @@ -41,31 +35,55 @@ class PlanningDisplayScreen extends React.Component { displayData: Object; shouldFetchData: boolean; eventId: number; + errorCode: number; colors: Object; - state = { - - }; - constructor(props) { super(props); this.colors = props.theme.colors; if (this.props.route.params.data !== undefined) { this.displayData = this.props.route.params.data; - this.eventId = this.props.route.params.data.eventId; + console.log(this.displayData); + this.eventId = this.displayData.id; this.shouldFetchData = false; + this.errorCode = 0; + this.state = { + loading: false, + }; } else { - this.displayData = FAKE_EVENT; + this.displayData = null; this.eventId = this.props.route.params.eventId; this.shouldFetchData = true; - console.log(this.eventId); + this.errorCode = 0; + this.state = { + loading: true, + }; + this.fetchData(); + } } - render() { - // console.log("rendering planningDisplayScreen"); + fetchData = () => { + this.setState({loading: true}); + apiRequest(CLUB_INFO_PATH, 'POST', {id: this.eventId}) + .then(this.onFetchSuccess) + .catch(this.onFetchError); + }; + + onFetchSuccess = (data: Object) => { + this.displayData = data; + console.log(this.displayData); + this.setState({loading: false}); + }; + + onFetchError = (error: number) => { + this.errorCode = error; + this.setState({loading: false}); + }; + + getContent() { let subtitle = getFormattedEventTime( this.displayData["date_begin"], this.displayData["date_end"]); let dateString = getDateOnlyString(this.displayData["date_begin"]); @@ -106,6 +124,15 @@ class PlanningDisplayScreen extends React.Component { ); } + + render() { + if (this.state.loading) + return ; + else if (this.errorCode === 0) + return this.getContent(); + else + return ; + } } export default withTheme(PlanningDisplayScreen); diff --git a/src/utils/WebData.js b/src/utils/WebData.js index 7261aa2..3f354e3 100644 --- a/src/utils/WebData.js +++ b/src/utils/WebData.js @@ -1,5 +1,65 @@ // @flow +export const ERROR_TYPE = { + SUCCESS: 0, + BAD_CREDENTIALS: 1, + BAD_TOKEN: 2, + NO_CONSENT: 3, + BAD_INPUT: 400, + FORBIDDEN: 403, + CONNECTION_ERROR: 404, + SERVER_ERROR: 500, + UNKNOWN: 999, +}; + +type response_format = { + error: number, + data: Object, +} + +const API_ENDPOINT = "https://www.amicale-insat.fr/api/"; + +export async function apiRequest(path: string, method: string, params: ?Object) { + 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.CONNECTION_ERROR); + }) + .catch(() => { + reject(ERROR_TYPE.CONNECTION_ERROR); + }); + }); +} + +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; +} + /** * Read data from FETCH_URL and return it. * If no data was found, returns an empty object