Implemented base vote screen and updated ConnectionManager.js to match new protocol

This commit is contained in:
Arnaud Vergnet 2020-04-06 18:32:10 +02:00
parent 7b332e11fc
commit e1a57487a2
9 changed files with 358 additions and 182 deletions

View file

@ -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);
});

View file

@ -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<Props, State> {
currentUserToken: string | null;
connectionManager: ConnectionManager;
errorCode: number;
data: Object;
data: Array<Object>;
colors: Object;
constructor(props) {
@ -35,6 +35,7 @@ class AuthenticatedScreen extends React.Component<Props, State> {
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<Props, State> {
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<Props, State> {
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<Props, State> {
return (
this.state.loading
? <BasicLoadingScreen/>
: (this.data !== undefined
: (this.allRequestsValid()
? this.props.renderFunction(this.data)
: this.getErrorRender())
);

View file

@ -69,6 +69,12 @@ class SideBar extends React.Component<Props, State> {
icon: "account-group",
onlyWhenLoggedIn: true,
},
{
name: "VOTE",
route: "VoteScreen",
icon: "vote",
onlyWhenLoggedIn: true,
},
{
name: i18n.t('screens.logout'),
route: 'disconnect',

View file

@ -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);
});
}
}

View file

@ -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 (
<VoteStack.Navigator
initialRouteName="VoteScreen"
headerMode="float"
screenOptions={defaultScreenOptions}
>
<VoteStack.Screen
name="VoteScreen"
component={VoteScreen}
options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: "VoteScreen",
headerLeft: openDrawer
};
}}
/>
</VoteStack.Navigator>
);
}
const ClubStack = createStackNavigator();
function ClubStackComponent() {
@ -341,6 +367,10 @@ export default function DrawerNavigator() {
name="ClubListScreen"
component={ClubStackComponent}
/>
<Drawer.Screen
name="VoteScreen"
component={VoteStackComponent}
/>
</Drawer.Navigator>
);
}

View file

@ -71,7 +71,7 @@ class ClubListScreen extends React.Component<Props, State> {
* @return {*}
*/
getHeaderButtons = () => {
const onPress = () => this.props.navigation.navigate( "ClubAboutScreen");
const onPress = () => this.props.navigation.navigate("ClubAboutScreen");
return <HeaderButton icon={'information'} onPress={onPress}/>;
};
@ -91,11 +91,11 @@ class ClubListScreen extends React.Component<Props, State> {
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
<FlatList
data={data.clubs}
data={data[0].clubs}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
@ -193,7 +193,12 @@ class ClubListScreen extends React.Component<Props, State> {
return (
<AuthenticatedScreen
{...this.props}
link={'https://www.amicale-insat.fr/api/clubs/list'}
links={[
{
link: 'clubs/list',
mandatory: true,
}
]}
renderFunction={this.getScreen}
/>
);

View file

@ -149,18 +149,18 @@ class LoginScreen extends React.Component<Props, State> {
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;

View file

@ -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<Props, State> {
data: Object;
flatListData: Array<Object>;
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<Props, State> {
});
}
showDisconnectDialog = () => this.setState({ dialogVisible: true });
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({ dialogVisible: false });
hideDisconnectDialog = () => this.setState({dialogVisible: false});
getHeaderButtons() {
return <HeaderButton icon={'logout'} onPress={this.showDisconnectDialog}/>;
}
getScreen(data: Object) {
this.data = data;
getScreen = (data: Object) => {
this.data = data[0];
return (
<View>
<FlatList
renderItem={item => this.getRenderItem(item)}
keyExtractor={item => item.id}
data={this.flatListData}
/>
<ScrollView>
{this.getPersonalCard()}
{this.getClubCard()}
{this.getMembershipCar()}
<LogoutDialog
{...this.props}
visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
</ScrollView>
)
}
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<Props, State> {
return (
<AuthenticatedScreen
{...this.props}
link={'https://www.amicale-insat.fr/api/user/profile'}
renderFunction={(data) => this.getScreen(data)}
links={[
{
link: 'user/profile',
mandatory: true,
}
]}
renderFunction={this.getScreen}
/>
);
}

View file

@ -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<Props, State> {
state = {};
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
getScreen = (data: Object) => {
console.log(data);
return (
<ScrollView>
{this.getTitleCard()}
{this.getVoteCard()}
</ScrollView>
);
};
getTitleCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTE"}
subtitle={"WHY"}
left={(props) => <Avatar.Image
{...props}
source={ICON_AMICALE}
style={styles.icon}
/>}
/>
<Card.Content>
<Paragraph>
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.
</Paragraph>
<Paragraph>
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.
</Paragraph>
</Card.Content>
</Card>
);
}
getVoteCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTE OPEN"}
subtitle={"VALID UNTIL DATE END"}
/>
<Card.Content>
<Paragraph>TEAM1</Paragraph>
<Paragraph>TEAM2</Paragraph>
</Card.Content>
</Card>
);
}
getVoteResultCard() {
return (
<Card style={styles.card}>
<Card.Title
title={"VOTE RESULTS"}
subtitle={"DATE END RESULTS"}
/>
<Card.Content>
<Paragraph>TEAM1</Paragraph>
<Paragraph>TEAM2</Paragraph>
</Card.Content>
</Card>
);
}
getEmptyVoteCard() {
}
render() {
return (
<AuthenticatedScreen
{...this.props}
links={[
{
link: 'elections/teams',
mandatory: false,
},
{
link: 'elections/dates',
mandatory: true,
},
]}
renderFunction={this.getScreen}
/>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});
export default withTheme(VoteScreen);