Compare commits

...

4 commits

7 changed files with 417 additions and 73 deletions

View file

@ -1,15 +1,143 @@
import React from 'react';
import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
import * as SecureStore from 'expo-secure-store';
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
const c = ConnectionManager.getInstance();
test("connect bad credentials", () => {
return expect(c.connect('truc', 'chose'))
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: false,
message: 'Adresse mail ou mot de passe incorrect',
token: ''
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS);
});
test("connect good credentials", () => {
return expect(c.connect('vergnet@etud.insa-toulouse.fr', 'Coucoù512'))
.resolves.toBe('test');
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: true,
message: 'Connexion confirmée',
token: 'token'
}
},
})
});
jest.spyOn(SecureStore, 'setItemAsync').mockImplementationOnce(() => {
return Promise.resolve(true);
});
return expect(c.connect('email', 'password')).resolves.toBeTruthy();
});
test("connect good credentials, fail save token", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: true,
message: 'Connexion confirmée',
token: 'token'
}
},
})
});
jest.spyOn(SecureStore, 'setItemAsync').mockImplementationOnce(() => {
return Promise.reject(false);
});
return expect(c.connect('email', 'password')).rejects.toBe(ERROR_TYPE.SAVE_TOKEN);
});
test("connect connection error", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject();
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("connect bogus response 1", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
thing: true,
wrong: '',
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("connect bogus response 2", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: true,
message: '',
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("connect bogus response 3", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: false,
message: '',
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS);
});
test("connect bogus response 4", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
message: '',
token: '',
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("connect bogus response 5", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: true,
message: 'Connexion confirmée',
token: ''
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});

View file

@ -3,16 +3,17 @@
import * as React from 'react';
import {Dimensions, FlatList, Image, Platform, StyleSheet, View} from 'react-native';
import i18n from "i18n-js";
import * as WebBrowser from 'expo-web-browser';
import {openBrowser} from "../utils/WebBrowser";
import SidebarDivider from "./SidebarDivider";
import SidebarItem from "./SidebarItem";
import {TouchableRipple} from "react-native-paper";
import {TouchableRipple, withTheme} from "react-native-paper";
const deviceWidth = Dimensions.get("window").width;
type Props = {
navigation: Object,
state: Object,
theme: Object,
};
type State = {
@ -22,7 +23,7 @@ type State = {
/**
* Component used to render the drawer menu content
*/
export default class SideBar extends React.PureComponent<Props, State> {
class SideBar extends React.PureComponent<Props, State> {
dataSet: Array<Object>;
@ -31,6 +32,7 @@ export default class SideBar extends React.PureComponent<Props, State> {
};
getRenderItem: Function;
colors: Object;
/**
* Generate the dataset
@ -126,6 +128,7 @@ export default class SideBar extends React.PureComponent<Props, State> {
},
];
this.getRenderItem = this.getRenderItem.bind(this);
this.colors = props.theme.colors;
}
/**
@ -138,7 +141,7 @@ export default class SideBar extends React.PureComponent<Props, State> {
if (item.link === undefined)
this.props.navigation.navigate(item.route);
else
WebBrowser.openBrowserAsync(item.link);
openBrowser(item.link, this.colors.primary);
}
/**
@ -218,3 +221,5 @@ const styles = StyleSheet.create({
marginTop: Platform.OS === "android" ? -3 : undefined
}
});
export default withTheme(SideBar);

View file

@ -1,8 +1,11 @@
// @flow
import * as SecureStore from 'expo-secure-store';
export const ERROR_TYPE = {
BAD_CREDENTIALS: 0,
CONNECTION_ERROR: 1
CONNECTION_ERROR: 1,
SAVE_TOKEN: 2,
};
const AUTH_URL = "https://www.amicale-insat.fr/api/password";
@ -27,6 +30,20 @@ export default class ConnectionManager {
ConnectionManager.instance;
}
async saveLogin(email: string, token: string) {
this.#token = token;
this.#email = email;
return new Promise((resolve, reject) => {
SecureStore.setItemAsync('token', token)
.then(() => {
resolve(true);
})
.catch(error => {
reject(false);
});
});
}
async connect(email: string, password: string) {
let data = {
email: email,
@ -42,24 +59,31 @@ export default class ConnectionManager {
body: JSON.stringify(data)
}).then(async (response) => response.json())
.then((data) => {
console.log(data);
if (this.isResponseValid(data))
resolve({success: data.success, token: data.token});
else
reject(ERROR_TYPE.BAD_CREDENTIALS);
if (this.isResponseValid(data)) {
if (data.state) {
this.saveLogin(email, data.token)
.then(() => {
resolve(true);
})
.catch(() => {
reject(ERROR_TYPE.SAVE_TOKEN);
});
} else
reject(ERROR_TYPE.BAD_CREDENTIALS);
} else
reject(ERROR_TYPE.CONNECTION_ERROR);
})
.catch((error) => {
console.log(error);
reject(ERROR_TYPE.CONNECTION_ERROR);
});
});
}
isResponseValid(response: Object) {
return response !== undefined
&& response.success !== undefined
&& response.success
&& response.token !== undefined;
let valid = response !== undefined && response.state !== undefined;
if (valid && response.state)
valid = valid && response.token !== undefined && response.token !== '';
return valid;
}
}

View file

@ -12,6 +12,9 @@
},
"jest": {
"preset": "react-native",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base)"
],
"setupFilesAfterEnv": [
"jest-extended"
]

View file

@ -1,9 +1,18 @@
// @flow
import * as React from 'react';
import {Keyboard, KeyboardAvoidingView, StyleSheet, TouchableWithoutFeedback, View} from "react-native";
import {Button, Text, TextInput, Title} from 'react-native-paper';
import {
Alert,
Keyboard,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
TouchableWithoutFeedback,
View
} from "react-native";
import {Avatar, Button, Card, HelperText, Text, TextInput, withTheme} from 'react-native-paper';
import ConnectionManager from "../../managers/ConnectionManager";
import {openBrowser} from "../../utils/WebBrowser";
type Props = {
navigation: Object,
@ -12,79 +21,239 @@ type Props = {
type State = {
email: string,
password: string,
isEmailValidated: boolean,
isPasswordValidated: boolean,
loading: boolean,
}
const ICON_AMICALE = require('../../assets/amicale.png');
export default class LoginScreen extends React.Component<Props, State> {
const RESET_PASSWORD_LINK = "https://www.amicale-insat.fr/password/reset";
const emailRegex = /^.+@.+\..+$/;
class LoginScreen extends React.Component<Props, State> {
state = {
email: '',
password: '',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
};
colors: Object;
onEmailChange: Function;
onPasswordChange: Function;
validateEmail: Function;
validatePassword: Function;
onSubmit: Function;
onEmailSubmit: Function;
onResetPasswordClick: Function;
constructor() {
super();
passwordInputRef: Object;
constructor(props) {
super(props);
this.onEmailChange = this.onInputChange.bind(this, true);
this.onPasswordChange = this.onInputChange.bind(this, false);
this.validateEmail = this.validateEmail.bind(this);
this.validatePassword = this.validatePassword.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onEmailSubmit = this.onEmailSubmit.bind(this);
this.onResetPasswordClick = this.onResetPasswordClick.bind(this);
this.colors = props.theme.colors;
}
onResetPasswordClick() {
openBrowser(RESET_PASSWORD_LINK, this.colors.primary);
}
validateEmail() {
this.setState({isEmailValidated: true});
}
isEmailValid() {
return emailRegex.test(this.state.email);
}
shouldShowEmailError() {
return this.state.isEmailValidated && !this.isEmailValid();
}
validatePassword() {
this.setState({isPasswordValidated: true});
}
isPasswordValid() {
return this.state.password !== '';
}
shouldShowPasswordError() {
return this.state.isPasswordValidated && !this.isPasswordValid();
}
shouldEnableLogin() {
return this.isEmailValid() && this.isPasswordValid();
}
onInputChange(isEmail: boolean, value: string) {
if (isEmail)
this.setState({email: value});
else
this.setState({password: value});
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
onEmailSubmit() {
this.passwordInputRef.focus();
}
onSubmit() {
console.log('pressed');
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
if (this.shouldEnableLogin()) {
this.setState({loading: true});
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
.then((data) => {
console.log(data);
Alert.alert('COOL', 'ÇA MARCHE');
})
.catch((error) => {
console.log(error);
Alert.alert('ERREUR', 'MDP OU MAIL INVALIDE');
})
.finally(() => {
this.setState({loading: false});
});
}
}
getFormInput() {
return (
<View>
<TextInput
label='Email'
mode='outlined'
value={this.state.email}
onChangeText={this.onEmailChange}
onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()}
textContentType={'emailAddress'}
autoCapitalize={'none'}
autoCompleteType={'email'}
autoCorrect={false}
keyboardType={'email-address'}
returnKeyType={'next'}
secureTextEntry={false}
/>
<HelperText
type="error"
visible={this.shouldShowEmailError()}
>
EMAIL INVALID
</HelperText>
<TextInput
ref={(ref) => {
this.passwordInputRef = ref;
}}
label='Password'
mode='outlined'
value={this.state.password}
onChangeText={this.onPasswordChange}
onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()}
textContentType={'password'}
autoCapitalize={'none'}
autoCompleteType={'password'}
autoCorrect={false}
keyboardType={'default'}
returnKeyType={'done'}
secureTextEntry={true}
/>
<HelperText
type="error"
visible={this.shouldShowPasswordError()}
>
PLS ENTER PASSWORD
</HelperText>
</View>
);
}
getMainCard() {
return (
<Card style={styles.card}>
<Card.Title
title="COUCOU"
subtitle="ENTREZ VOS IDENTIFIANTS"
left={(props) => <Avatar.Image
{...props}
source={ICON_AMICALE}
style={{backgroundColor: 'transparent'}}/>}
/>
<Card.Content>
{this.getFormInput()}
<Card.Actions>
<Button
icon="send"
mode="contained"
disabled={!this.shouldEnableLogin()}
loading={this.state.loading}
onPress={this.onSubmit}
style={{marginLeft: 'auto'}}>
LOGIN
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
getSecondaryCard() {
return (
<Card style={styles.card}>
<Card.Content>
<Text>MDP OUBLIÉ ? t'es pas doué</Text>
<View style={styles.btnContainer}>
<Button
icon="reload"
mode="contained"
onPress={this.onResetPasswordClick}
style={{marginLeft: 'auto'}}>
RESET MDP
</Button>
</View>
<Text>PAS DE COMPTE ? DOMMAGE PASSE À L'AMICALE</Text>
</Card.Content>
</Card>
);
}
render() {
return (
<KeyboardAvoidingView
behavior={"padding"}
behavior={"height"}
contentContainerStyle={styles.container}
style={styles.container}
enabled
keyboardVerticalOffset={100}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.inner}>
<Title>COUCOU</Title>
<Text>entrez vos identifiants</Text>
<TextInput
label='Email'
mode='outlined'
value={this.state.email}
onChangeText={this.onEmailChange}
/>
<TextInput
label='Password'
mode='outlined'
value={this.state.password}
onChangeText={this.onPasswordChange}
/>
<View style={styles.btnContainer}>
<Button icon="send" onPress={() => this.onSubmit()}>
LOGIN
</Button>
<ScrollView>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View>
{this.getMainCard()}
{this.getSecondaryCard()}
</View>
<Text>Pas de compte, dommage !</Text>
<View style={styles.btnContainer}>
<Button icon="send" onPress={() => console.log('Pressed')}>
Créer un compte
</Button>
</View>
</View>
</TouchableWithoutFeedback>
</TouchableWithoutFeedback>
</ScrollView>
</KeyboardAvoidingView>
);
}
@ -92,11 +261,12 @@ export default class LoginScreen extends React.Component<Props, State> {
const styles = StyleSheet.create({
container: {
flex: 1
},
inner: {
padding: 24,
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
card: {
margin: 10,
},
header: {
fontSize: 36,
@ -104,6 +274,9 @@ const styles = StyleSheet.create({
},
textInput: {},
btnContainer: {
marginTop: 12
marginTop: 5,
marginBottom: 10,
}
});
export default withTheme(LoginScreen);

View file

@ -4,13 +4,13 @@ import * as React from 'react';
import {View} from 'react-native';
import i18n from "i18n-js";
import DashboardItem from "../components/EventDashboardItem";
import * as WebBrowser from 'expo-web-browser';
import WebSectionList from "../components/WebSectionList";
import {Text, withTheme} from 'react-native-paper';
import FeedItem from "../components/FeedItem";
import SquareDashboardItem from "../components/SquareDashboardItem";
import PreviewEventDashboardItem from "../components/PreviewEventDashboardItem";
import {stringToDate} from "../utils/Planning";
import {openBrowser} from "../utils/WebBrowser";
// import DATA from "../dashboard_data.json";
@ -70,7 +70,7 @@ class HomeScreen extends React.Component<Props> {
}
onTutorInsaClick() {
WebBrowser.openBrowserAsync("https://www.etud.insa-toulouse.fr/~tutorinsa/");
openBrowser("https://www.etud.insa-toulouse.fr/~tutorinsa/", this.colors.primary);
}
onProximoClick() {
@ -402,7 +402,7 @@ class HomeScreen extends React.Component<Props> {
}
openLink(link: string) {
WebBrowser.openBrowserAsync(link);
openBrowser(link, this.colors.primary);
}
/**

11
utils/WebBrowser.js Normal file
View file

@ -0,0 +1,11 @@
// @flow
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
export function openBrowser(url: string, color: string) {
WebBrowser.openBrowserAsync(url, {
toolbarColor: color,
enableBarCollapsing: true,
});
}