From e1a57487a2cdfd09cfc6e59098420d5073c4d06b Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Mon, 6 Apr 2020 18:32:10 +0200 Subject: [PATCH] Implemented base vote screen and updated ConnectionManager.js to match new protocol --- __tests__/managers/ConnectionManager.test.js | 138 +++++++----------- src/components/Amicale/AuthenticatedScreen.js | 61 ++++++-- src/components/Sidebar/Sidebar.js | 6 + src/managers/ConnectionManager.js | 97 ++++++------ src/navigation/DrawerNavigator.js | 30 ++++ src/screens/Amicale/Clubs/ClubListScreen.js | 13 +- src/screens/Amicale/LoginScreen.js | 12 +- src/screens/Amicale/ProfileScreen.js | 53 +++---- src/screens/Amicale/VoteScreen.js | 130 +++++++++++++++++ 9 files changed, 358 insertions(+), 182 deletions(-) create mode 100644 src/screens/Amicale/VoteScreen.js diff --git a/__tests__/managers/ConnectionManager.test.js b/__tests__/managers/ConnectionManager.test.js index 024af03..0e3d9e7 100644 --- a/__tests__/managers/ConnectionManager.test.js +++ b/__tests__/managers/ConnectionManager.test.js @@ -65,84 +65,64 @@ test('recoverLogin success saved', () => { test('isRequestResponseValid', () => { let json = { - state: true, + error: 0, data: {} }; - expect(c.isRequestResponseValid(json)).toBeTrue(); + expect(c.isResponseValid(json)).toBeTrue(); json = { - state: false, + error: 1, data: {} }; - expect(c.isRequestResponseValid(json)).toBeTrue(); + expect(c.isResponseValid(json)).toBeTrue(); json = { - state: false, - message: 'coucou', + error: 50, + data: {} + }; + expect(c.isResponseValid(json)).toBeTrue(); + json = { + error: 50, data: {truc: 'machin'} }; - expect(c.isRequestResponseValid(json)).toBeTrue(); + expect(c.isResponseValid(json)).toBeTrue(); json = { message: 'coucou' }; - expect(c.isRequestResponseValid(json)).toBeFalse(); + expect(c.isResponseValid(json)).toBeFalse(); json = { - state: 'coucou' + error: 'coucou', + data: {truc: 'machin'} }; - expect(c.isRequestResponseValid(json)).toBeFalse(); + expect(c.isResponseValid(json)).toBeFalse(); json = { - state: true, + error: 0, + data: 'coucou' }; - expect(c.isRequestResponseValid(json)).toBeFalse(); + expect(c.isResponseValid(json)).toBeFalse(); + json = { + error: 0, + }; + expect(c.isResponseValid(json)).toBeFalse(); }); test("isConnectionResponseValid", () => { let json = { - state: true, - message: 'Connexion confirmée', - token: 'token' + error: 0, + data: {token: 'token'} }; expect(c.isConnectionResponseValid(json)).toBeTrue(); json = { - state: true, - token: 'token' + error: 2, + data: {} }; expect(c.isConnectionResponseValid(json)).toBeTrue(); json = { - state: false, - }; - expect(c.isConnectionResponseValid(json)).toBeTrue(); - json = { - state: false, - message: 'Adresse mail ou mot de passe incorrect', - token: '' - }; - expect(c.isConnectionResponseValid(json)).toBeTrue(); - json = { - state: true, - message: 'Connexion confirmée', - token: '' + error: 0, + data: {token: ''} }; expect(c.isConnectionResponseValid(json)).toBeFalse(); json = { - state: true, - message: 'Connexion confirmée', - }; - expect(c.isConnectionResponseValid(json)).toBeFalse(); - json = { - state: 'coucou', - message: 'Connexion confirmée', - token: 'token' - }; - expect(c.isConnectionResponseValid(json)).toBeFalse(); - json = { - state: true, - message: 'Connexion confirmée', - token: 2 - }; - expect(c.isConnectionResponseValid(json)).toBeFalse(); - json = { - coucou: 'coucou', - message: 'Connexion confirmée', - token: 'token' + error: 'prout', + data: {token: ''} }; expect(c.isConnectionResponseValid(json)).toBeFalse(); }); @@ -152,10 +132,9 @@ test("connect bad credentials", () => { return Promise.resolve({ json: () => { return { - state: false, - message: 'Adresse mail ou mot de passe incorrect', - token: '' - } + error: ERROR_TYPE.BAD_CREDENTIALS, + data: {} + }; }, }) }); @@ -168,10 +147,9 @@ test("connect good credentials", () => { return Promise.resolve({ json: () => { return { - state: true, - message: 'Connexion confirmée', - token: 'token' - } + error: ERROR_TYPE.SUCCESS, + data: {token: 'token'} + }; }, }) }); @@ -186,13 +164,9 @@ test("connect good credentials no consent", () => { return Promise.resolve({ json: () => { return { - state: false, - message: 'pas de consent', - token: '', - data: { - consent: false, - } - } + error: ERROR_TYPE.NO_CONSENT, + data: {} + }; }, }) }); @@ -205,17 +179,16 @@ test("connect good credentials, fail save token", () => { return Promise.resolve({ json: () => { return { - state: true, - message: 'Connexion confirmée', - token: 'token' - } + error: ERROR_TYPE.SUCCESS, + data: {token: 'token'} + }; }, }) }); jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => { return Promise.reject(false); }); - return expect(c.connect('email', 'password')).rejects.toBe(ERROR_TYPE.SAVE_TOKEN); + return expect(c.connect('email', 'password')).rejects.toBe(ERROR_TYPE.UNKNOWN); }); test("connect connection error", () => { @@ -249,7 +222,10 @@ test("authenticatedRequest success", () => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => { return Promise.resolve({ json: () => { - return {state: true, message: 'Connexion vérifiée', data: {coucou: 'toi'}} + return { + error: ERROR_TYPE.SUCCESS, + data: {coucou: 'toi'} + }; }, }) }); @@ -264,12 +240,15 @@ test("authenticatedRequest error wrong token", () => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => { return Promise.resolve({ json: () => { - return {state: false, message: 'Le champ token sélectionné est invalide.'} + return { + error: ERROR_TYPE.BAD_TOKEN, + data: {} + }; }, }) }); return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')) - .rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS); + .rejects.toBe(ERROR_TYPE.BAD_TOKEN); }); test("authenticatedRequest error bogus response", () => { @@ -279,7 +258,9 @@ test("authenticatedRequest error bogus response", () => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => { return Promise.resolve({ json: () => { - return {state: true, message: 'Connexion vérifiée'} + return { + error: ERROR_TYPE.SUCCESS, + }; }, }) }); @@ -302,13 +283,6 @@ test("authenticatedRequest error no token", () => { jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { return null; }); - jest.spyOn(global, 'fetch').mockImplementationOnce(() => { - return Promise.resolve({ - json: () => { - return {state: false, message: 'Le champ token sélectionné est invalide.'} - }, - }) - }); return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')) - .rejects.toBe(ERROR_TYPE.NO_TOKEN); + .rejects.toBe(ERROR_TYPE.UNKNOWN); }); diff --git a/src/components/Amicale/AuthenticatedScreen.js b/src/components/Amicale/AuthenticatedScreen.js index dfb06b1..634adbb 100644 --- a/src/components/Amicale/AuthenticatedScreen.js +++ b/src/components/Amicale/AuthenticatedScreen.js @@ -10,7 +10,7 @@ import BasicLoadingScreen from "../Custom/BasicLoadingScreen"; type Props = { navigation: Object, theme: Object, - link: string, + links: Array<{link: string, mandatory: boolean}>, renderFunction: Function, } @@ -27,7 +27,7 @@ class AuthenticatedScreen extends React.Component { currentUserToken: string | null; connectionManager: ConnectionManager; errorCode: number; - data: Object; + data: Array; colors: Object; constructor(props) { @@ -35,6 +35,7 @@ class AuthenticatedScreen extends React.Component { this.colors = props.theme.colors; this.connectionManager = ConnectionManager.getInstance(); this.props.navigation.addListener('focus', this.onScreenFocus.bind(this)); + this.data = new Array(this.props.links.length); } onScreenFocus() { @@ -46,25 +47,53 @@ class AuthenticatedScreen extends React.Component { if (!this.state.loading) this.setState({loading: true}); if (this.connectionManager.isLoggedIn()) { - this.connectionManager.authenticatedRequest(this.props.link) - .then((data) => { - this.onFinishedLoading(data, -1); - }) - .catch((error) => { - this.onFinishedLoading(undefined, error); - }); + for (let i = 0; i < this.props.links.length; i++) { + this.connectionManager.authenticatedRequest(this.props.links[i].link) + .then((data) => { + this.onFinishedLoading(data, i, -1); + }) + .catch((error) => { + this.onFinishedLoading(null, i, error); + }); + } + } else { - this.onFinishedLoading(undefined, ERROR_TYPE.BAD_CREDENTIALS); + this.onFinishedLoading(null, -1, ERROR_TYPE.BAD_CREDENTIALS); } }; - onFinishedLoading(data: Object, error: number) { - this.data = data; + onFinishedLoading(data: Object, index: number, error: number) { + if (index >= 0 && index < this.props.links.length) + this.data[index] = data; this.currentUserToken = data !== undefined ? this.connectionManager.getToken() : null; this.errorCode = error; - this.setState({loading: false}); + + if (this.allRequestsFinished()) + this.setState({loading: false}); + } + + allRequestsFinished() { + let finished = true; + for (let i = 0; i < this.data.length; i++) { + if (this.data[i] === undefined) { + finished = false; + break; + } + } + return finished; + } + + allRequestsValid() { + let valid = true; + for (let i = 0; i < this.data.length; i++) { + if (this.data[i] === null && this.props.links[i].mandatory) { + valid = false; + break; + } + } + return valid; } getErrorRender() { @@ -75,6 +104,10 @@ class AuthenticatedScreen extends React.Component { message = i18n.t("loginScreen.errors.credentials"); icon = "account-alert-outline"; break; + case ERROR_TYPE.BAD_TOKEN: + message = "BAD TOKEN"; // TODO translate + icon = "access-point-network-off"; + break; case ERROR_TYPE.CONNECTION_ERROR: message = i18n.t("loginScreen.errors.connection"); icon = "access-point-network-off"; @@ -99,7 +132,7 @@ class AuthenticatedScreen extends React.Component { return ( this.state.loading ? - : (this.data !== undefined + : (this.allRequestsValid() ? this.props.renderFunction(this.data) : this.getErrorRender()) ); diff --git a/src/components/Sidebar/Sidebar.js b/src/components/Sidebar/Sidebar.js index bc8734d..6f4b016 100644 --- a/src/components/Sidebar/Sidebar.js +++ b/src/components/Sidebar/Sidebar.js @@ -69,6 +69,12 @@ class SideBar extends React.Component { icon: "account-group", onlyWhenLoggedIn: true, }, + { + name: "VOTE", + route: "VoteScreen", + icon: "vote", + onlyWhenLoggedIn: true, + }, { name: i18n.t('screens.logout'), route: 'disconnect', diff --git a/src/managers/ConnectionManager.js b/src/managers/ConnectionManager.js index 5965450..84d2a3a 100644 --- a/src/managers/ConnectionManager.js +++ b/src/managers/ConnectionManager.js @@ -3,14 +3,35 @@ import * as SecureStore from 'expo-secure-store'; export const ERROR_TYPE = { - BAD_CREDENTIALS: 0, - CONNECTION_ERROR: 1, - SAVE_TOKEN: 2, - NO_TOKEN: 3, - NO_CONSENT: 4, + SUCCESS: 0, + BAD_CREDENTIALS: 1, + BAD_TOKEN: 2, + NO_CONSENT: 3, + BAD_INPUT: 400, + FORBIDDEN: 403, + CONNECTION_ERROR: 404, + SERVER_ERROR: 500, + UNKNOWN: 999, }; -const AUTH_URL = "https://www.amicale-insat.fr/api/password"; +type response_format = { + error: number, + data: Object, +} + +/** + * champ: error + * + * 0 : SUCCESS -> pas d'erreurs + * 1 : BAD_CREDENTIALS -> email ou mdp invalide + * 2 : BAD_TOKEN -> session expirée + * 3 : NO_CONSENT + * 403 : FORBIDDEN -> accès a la ressource interdit + * 500 : SERVER_ERROR -> pb coté serveur + */ + +const API_ENDPOINT = "https://www.amicale-insat.fr/api/"; +const AUTH_PATH = "password"; export default class ConnectionManager { static instance: ConnectionManager | null = null; @@ -110,7 +131,7 @@ export default class ConnectionManager { password: password, }; return new Promise((resolve, reject) => { - fetch(AUTH_URL, { + fetch(API_ENDPOINT + AUTH_PATH, { method: 'POST', headers: new Headers({ 'Accept': 'application/json', @@ -118,22 +139,18 @@ export default class ConnectionManager { }), body: JSON.stringify(data) }).then(async (response) => response.json()) - .then((data) => { - if (this.isConnectionResponseValid(data)) { - if (data.state) { - this.saveLogin(email, data.token) + .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.SAVE_TOKEN); + reject(ERROR_TYPE.UNKNOWN); }); - } else if (data.data !== undefined - && data.data.consent !== undefined - && !data.data.consent) - reject(ERROR_TYPE.NO_CONSENT); - else - reject(ERROR_TYPE.BAD_CREDENTIALS); + } else + reject(response.error); } else reject(ERROR_TYPE.CONNECTION_ERROR); }) @@ -143,35 +160,32 @@ export default class ConnectionManager { }); } - isRequestResponseValid(response: Object) { + isResponseValid(response: response_format) { let valid = response !== undefined - && response.state !== undefined - && typeof response.state === "boolean"; + && response.error !== undefined + && typeof response.error === "number"; - if (valid && response.state) - valid = valid - && response.data !== undefined - && typeof response.data === "object"; + valid = valid + && response.data !== undefined + && typeof response.data === "object"; return valid; } - isConnectionResponseValid(response: Object) { - let valid = response !== undefined - && response.state !== undefined - && typeof response.state === "boolean"; + isConnectionResponseValid(response: response_format) { + let valid = this.isResponseValid(response); - if (valid && response.state) + if (valid && response.error === ERROR_TYPE.SUCCESS) valid = valid - && response.token !== undefined - && response.token !== '' - && typeof response.token === "string"; + && response.data.token !== undefined + && response.data.token !== '' + && typeof response.data.token === "string"; return valid; } - async authenticatedRequest(url: string) { + async authenticatedRequest(path: string) { return new Promise((resolve, reject) => { if (this.getToken() !== null) { - fetch(url, { + fetch(API_ENDPOINT + path, { method: 'POST', headers: new Headers({ 'Accept': 'application/json', @@ -179,12 +193,13 @@ export default class ConnectionManager { }), body: JSON.stringify({token: this.getToken()}) }).then(async (response) => response.json()) - .then((data) => { - if (this.isRequestResponseValid(data)) { - if (data.state) - resolve(data.data); + .then((response: response_format) => { + console.log(response); + if (this.isResponseValid(response)) { + if (response.error === ERROR_TYPE.SUCCESS) + resolve(response.data); else - reject(ERROR_TYPE.BAD_CREDENTIALS); + reject(response.error); } else reject(ERROR_TYPE.CONNECTION_ERROR); }) @@ -192,7 +207,7 @@ export default class ConnectionManager { reject(ERROR_TYPE.CONNECTION_ERROR); }); } else - reject(ERROR_TYPE.NO_TOKEN); + reject(ERROR_TYPE.UNKNOWN); }); } } diff --git a/src/navigation/DrawerNavigator.js b/src/navigation/DrawerNavigator.js index e311c6f..b40f180 100644 --- a/src/navigation/DrawerNavigator.js +++ b/src/navigation/DrawerNavigator.js @@ -20,6 +20,7 @@ import ProfileScreen from "../screens/Amicale/ProfileScreen"; import ClubListScreen from "../screens/Amicale/Clubs/ClubListScreen"; import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen"; import ClubAboutScreen from "../screens/Amicale/Clubs/ClubAboutScreen"; +import VoteScreen from "../screens/Amicale/VoteScreen"; const defaultScreenOptions = { gestureEnabled: true, @@ -239,6 +240,31 @@ function ProfileStackComponent() { ); } + +const VoteStack = createStackNavigator(); + +function VoteStackComponent() { + return ( + + { + const openDrawer = getDrawerButton.bind(this, navigation); + return { + title: "VoteScreen", + headerLeft: openDrawer + }; + }} + /> + + ); +} + const ClubStack = createStackNavigator(); function ClubStackComponent() { @@ -341,6 +367,10 @@ export default function DrawerNavigator() { name="ClubListScreen" component={ClubStackComponent} /> + ); } diff --git a/src/screens/Amicale/Clubs/ClubListScreen.js b/src/screens/Amicale/Clubs/ClubListScreen.js index a171c9e..9ae68ca 100644 --- a/src/screens/Amicale/Clubs/ClubListScreen.js +++ b/src/screens/Amicale/Clubs/ClubListScreen.js @@ -71,7 +71,7 @@ class ClubListScreen extends React.Component { * @return {*} */ getHeaderButtons = () => { - const onPress = () => this.props.navigation.navigate( "ClubAboutScreen"); + const onPress = () => this.props.navigation.navigate("ClubAboutScreen"); return ; }; @@ -91,11 +91,11 @@ class ClubListScreen extends React.Component { itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); getScreen = (data: Object) => { - this.categories = data.categories; + this.categories = data[0].categories; return ( //$FlowFixMe { return ( ); diff --git a/src/screens/Amicale/LoginScreen.js b/src/screens/Amicale/LoginScreen.js index 2fb3ba1..2866683 100644 --- a/src/screens/Amicale/LoginScreen.js +++ b/src/screens/Amicale/LoginScreen.js @@ -149,18 +149,18 @@ class LoginScreen extends React.Component { const title = i18n.t("loginScreen.errors.title"); let message; switch (error) { - case ERROR_TYPE.CONNECTION_ERROR: - message = i18n.t("loginScreen.errors.connection"); - break; case ERROR_TYPE.BAD_CREDENTIALS: message = i18n.t("loginScreen.errors.credentials"); break; - case ERROR_TYPE.SAVE_TOKEN: - message = i18n.t("loginScreen.errors.saveToken"); - break; case ERROR_TYPE.NO_CONSENT: message = i18n.t("loginScreen.errors.consent"); break; + case ERROR_TYPE.CONNECTION_ERROR: + message = i18n.t("loginScreen.errors.connection"); + break; + case ERROR_TYPE.SERVER_ERROR: + message = "SERVER ERROR"; // TODO translate + break; default: message = i18n.t("loginScreen.errors.unknown"); break; diff --git a/src/screens/Amicale/ProfileScreen.js b/src/screens/Amicale/ProfileScreen.js index aeb74a9..b01d17b 100644 --- a/src/screens/Amicale/ProfileScreen.js +++ b/src/screens/Amicale/ProfileScreen.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import {FlatList, StyleSheet, View} from "react-native"; +import {FlatList, ScrollView, StyleSheet} from "react-native"; import {Avatar, Button, Card, Divider, List, withTheme} from 'react-native-paper'; import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen"; import {openBrowser} from "../../utils/WebBrowser"; @@ -28,16 +28,9 @@ class ProfileScreen extends React.Component { data: Object; - flatListData: Array; - constructor(props) { super(props); this.colors = props.theme.colors; - this.flatListData = [ - {id: '0'}, - {id: '1'}, - {id: '2'}, - ] } componentDidMount() { @@ -47,44 +40,29 @@ class ProfileScreen extends React.Component { }); } - showDisconnectDialog = () => this.setState({ dialogVisible: true }); + showDisconnectDialog = () => this.setState({dialogVisible: true}); - hideDisconnectDialog = () => this.setState({ dialogVisible: false }); + hideDisconnectDialog = () => this.setState({dialogVisible: false}); getHeaderButtons() { return ; } - getScreen(data: Object) { - this.data = data; + getScreen = (data: Object) => { + this.data = data[0]; return ( - - this.getRenderItem(item)} - keyExtractor={item => item.id} - data={this.flatListData} - /> + + {this.getPersonalCard()} + {this.getClubCard()} + {this.getMembershipCar()} - - + ) - } - - getRenderItem({item}: Object): any { - switch (item.id) { - case '0': - return this.getPersonalCard(); - case '1': - return this.getClubCard(); - case '2': - return this.getMembershipCar(); - } - } - + }; getPersonalCard() { return ( @@ -232,8 +210,13 @@ class ProfileScreen extends React.Component { return ( this.getScreen(data)} + links={[ + { + link: 'user/profile', + mandatory: true, + } + ]} + renderFunction={this.getScreen} /> ); } diff --git a/src/screens/Amicale/VoteScreen.js b/src/screens/Amicale/VoteScreen.js new file mode 100644 index 0000000..c3df406 --- /dev/null +++ b/src/screens/Amicale/VoteScreen.js @@ -0,0 +1,130 @@ +// @flow + +import * as React from 'react'; +import {ScrollView, StyleSheet} from "react-native"; +import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper'; +import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen"; + +const ICON_AMICALE = require('../../../assets/amicale.png'); + +type Props = { + navigation: Object, + theme: Object, +} + +type State = {} + +class VoteScreen extends React.Component { + + state = {}; + + colors: Object; + + constructor(props) { + super(props); + this.colors = props.theme.colors; + } + + getScreen = (data: Object) => { + console.log(data); + return ( + + {this.getTitleCard()} + {this.getVoteCard()} + + ); + }; + + getTitleCard() { + return ( + + } + /> + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rhoncus porttitor + suscipit. Quisque hendrerit, quam id vestibulum vestibulum, lorem nisi hendrerit nisi, a + eleifend sapien diam ut elit. Curabitur sit amet vulputate lectus. Donec semper cursus sapien + vel finibus. + + + Sed et venenatis turpis. Fusce malesuada magna urna, sed vehicula sem luctus in. Vivamus + faucibus vel eros a ultricies. In sed laoreet ante, luctus mattis tellus. Etiam vitae ipsum + sagittis, consequat purus sed, blandit risus. + + + + ); + } + + getVoteCard() { + return ( + + + + TEAM1 + TEAM2 + + + ); + } + + getVoteResultCard() { + return ( + + + + TEAM1 + TEAM2 + + + ); + } + + getEmptyVoteCard() { + + } + + render() { + return ( + + ); + } +} + +const styles = StyleSheet.create({ + card: { + margin: 10, + }, + icon: { + backgroundColor: 'transparent' + }, +}); + +export default withTheme(VoteScreen);