Compare commits

..

No commits in common. "2010170e8ba3d57295147e1749bdd4545054c1c8" and "14856616df9f48d5814b64edae5108d4404a1ca3" have entirely different histories.

15 changed files with 21 additions and 1656 deletions

View file

@ -1,292 +0,0 @@
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 c = ConnectionManager.getInstance();
afterEach(() => {
jest.restoreAllMocks();
});
test('isLoggedIn yes', () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve(true);
});
return expect(c.isLoggedIn()).resolves.toBe(true);
});
test('isLoggedIn no', () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.reject(false);
});
return expect(c.isLoggedIn()).rejects.toBe(false);
});
test('recoverLogin error crypto', () => {
jest.spyOn(SecureStore, 'getItemAsync').mockImplementationOnce(() => {
return Promise.reject();
});
return expect(c.recoverLogin()).rejects.toBe(false);
});
test('recoverLogin success crypto', () => {
jest.spyOn(SecureStore, 'getItemAsync').mockImplementationOnce(() => {
return Promise.resolve('token1');
});
return expect(c.recoverLogin()).resolves.toBe('token1');
});
test('saveLogin success', () => {
jest.spyOn(SecureStore, 'setItemAsync').mockImplementationOnce(() => {
return Promise.resolve();
});
return expect(c.saveLogin('email', 'token2')).resolves.toBeTruthy();
});
test('saveLogin error', () => {
jest.spyOn(SecureStore, 'setItemAsync').mockImplementationOnce(() => {
return Promise.reject();
});
return expect(c.saveLogin('email', 'token3')).rejects.toBeFalsy();
});
test('recoverLogin error crypto with saved token', () => {
jest.spyOn(SecureStore, 'getItemAsync').mockImplementationOnce(() => {
return Promise.reject();
});
return expect(c.recoverLogin()).resolves.toBe('token2');
});
test('recoverLogin success saved', () => {
return expect(c.recoverLogin()).resolves.toBe('token2');
});
test('isRequestResponseValid', () => {
let json = {
state: true,
data: {}
};
expect(c.isRequestResponseValid(json)).toBeTrue();
json = {
state: false,
data: {}
};
expect(c.isRequestResponseValid(json)).toBeTrue();
json = {
state: false,
message: 'coucou',
data: {truc: 'machin'}
};
expect(c.isRequestResponseValid(json)).toBeTrue();
json = {
message: 'coucou'
};
expect(c.isRequestResponseValid(json)).toBeFalse();
json = {
state: 'coucou'
};
expect(c.isRequestResponseValid(json)).toBeFalse();
json = {
state: true,
};
expect(c.isRequestResponseValid(json)).toBeFalse();
});
test("isConnectionResponseValid", () => {
let json = {
state: true,
message: 'Connexion confirmée',
token: 'token'
};
expect(c.isConnectionResponseValid(json)).toBeTrue();
json = {
state: true,
token: 'token'
};
expect(c.isConnectionResponseValid(json)).toBeTrue();
json = {
state: false,
};
expect(c.isConnectionResponseValid(json)).toBeTrue();
json = {
state: true,
message: 'Connexion confirmée',
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'
};
expect(c.isConnectionResponseValid(json)).toBeFalse();
});
test("connect bad credentials", () => {
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", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: true,
message: 'Connexion confirmée',
token: 'token'
}
},
})
});
jest.spyOn(ConnectionManager.prototype, 'saveLogin').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(ConnectionManager.prototype, 'saveLogin').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("authenticatedRequest success", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {state: true, message: 'Connexion vérifiée', data: {coucou: 'toi'}}
},
})
});
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.resolves.toStrictEqual({coucou: 'toi'});
});
test("authenticatedRequest error wrong token", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
});
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.BAD_CREDENTIALS);
});
test("authenticatedRequest error bogus response", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {state: true, message: 'Connexion vérifiée'}
},
})
});
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("authenticatedRequest connection error", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject()
});
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("authenticatedRequest error no token", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.reject(false);
});
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);
});

View file

@ -1,37 +0,0 @@
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal, withTheme} from 'react-native-paper';
type Props = {
navigation: Object,
visible: boolean,
onDismiss: Function,
title: string,
message: string,
}
class AlertDialog extends React.PureComponent<Props> {
constructor(props) {
super(props);
}
render() {
return (
<Portal>
<Dialog
visible={this.props.visible}
onDismiss={this.props.onDismiss}>
<Dialog.Title>{this.props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{this.props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={this.props.onDismiss}>OK</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
}
export default withTheme(AlertDialog);

View file

@ -1,139 +0,0 @@
// @flow
import * as React from 'react';
import {View} from "react-native";
import {ActivityIndicator, withTheme} from 'react-native-paper';
import ConnectionManager, {ERROR_TYPE} from "../managers/ConnectionManager";
import NetworkErrorComponent from "./NetworkErrorComponent";
import i18n from 'i18n-js';
type Props = {
navigation: Object,
theme: Object,
link: string,
renderFunction: Function,
}
type State = {
loading: boolean,
}
class AuthenticatedScreen extends React.Component<Props, State> {
state = {
loading: true,
};
currentUserToken: string;
connectionManager: ConnectionManager;
errorCode: number;
data: Object;
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.connectionManager = ConnectionManager.getInstance();
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
this.fetchData();
}
onScreenFocus() {
if (this.currentUserToken !== this.connectionManager.getToken())
this.fetchData();
}
fetchData = () => {
if (!this.state.loading)
this.setState({loading: true});
this.connectionManager.isLoggedIn()
.then(() => {
this.connectionManager.authenticatedRequest(this.props.link)
.then((data) => {
this.onFinishedLoading(data, -1);
})
.catch((error) => {
this.onFinishedLoading(undefined, error);
});
})
.catch((error) => {
this.onFinishedLoading(undefined, ERROR_TYPE.BAD_CREDENTIALS);
});
};
onFinishedLoading(data: Object, error: number) {
this.data = data;
this.currentUserToken = data !== undefined
? this.connectionManager.getToken()
: '';
this.errorCode = error;
this.setState({loading: false});
}
/**
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading() {
return (
<View style={{
backgroundColor: this.colors.background,
position: 'absolute',
top: 0,
right: 0,
width: '100%',
height: '100%',
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}}>
<ActivityIndicator
animating={true}
size={'large'}
color={this.colors.primary}/>
</View>
);
}
getErrorRender() {
let message;
let icon;
switch (this.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
message = i18n.t("loginScreen.errors.credentials");
icon = "account-alert-outline";
break;
case ERROR_TYPE.CONNECTION_ERROR:
message = i18n.t("loginScreen.errors.connection");
icon = "access-point-network-off";
break;
default:
message = i18n.t("loginScreen.errors.unknown");
icon = "alert-circle-outline";
break;
}
return (
<NetworkErrorComponent
{...this.props}
icon={icon}
message={message}
onRefresh={this.fetchData}
/>
);
}
render() {
return (
this.state.loading
? this.getRenderLoading()
: (this.data !== undefined
? this.props.renderFunction(this.data)
: this.getErrorRender())
);
}
}
export default withTheme(AuthenticatedScreen);

View file

@ -1,83 +0,0 @@
// @flow
import * as React from 'react';
import {ActivityIndicator, Button, Dialog, Paragraph, Portal, withTheme} from 'react-native-paper';
import ConnectionManager from "../managers/ConnectionManager";
import i18n from 'i18n-js';
type Props = {
navigation: Object,
visible: boolean,
onDismiss: Function,
}
type State = {
loading: boolean,
}
class LogoutDialog extends React.PureComponent<Props, State> {
colors: Object;
state = {
loading: false,
};
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
onClickAccept = () => {
this.setState({loading: true});
ConnectionManager.getInstance().disconnect()
.then(() => {
this.props.onDismiss();
this.setState({loading: false});
this.props.navigation.reset({
index: 0,
routes: [{name: 'Main'}],
});
});
};
onDismiss = () => {
if (!this.state.loading)
this.props.onDismiss();
};
render() {
return (
<Portal>
<Dialog
visible={this.props.visible}
onDismiss={this.onDismiss}>
<Dialog.Title>
{this.state.loading
? i18n.t("dialog.disconnect.titleLoading")
: i18n.t("dialog.disconnect.title")}
</Dialog.Title>
<Dialog.Content>
{this.state.loading
? <ActivityIndicator
animating={true}
size={'large'}
color={this.colors.primary}/>
: <Paragraph>{i18n.t("dialog.disconnect.message")}</Paragraph>
}
</Dialog.Content>
{this.state.loading
? null
: <Dialog.Actions>
<Button onPress={this.onDismiss} style={{marginRight: 10}}>{i18n.t("dialog.cancel")}</Button>
<Button onPress={this.onClickAccept}>{i18n.t("dialog.yes")}</Button>
</Dialog.Actions>
}
</Dialog>
</Portal>
);
}
}
export default withTheme(LogoutDialog);

View file

@ -1,87 +0,0 @@
// @flow
import * as React from 'react';
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';
type Props = {
navigation: Object,
message: string,
icon: string,
onRefresh: Function,
}
type State = {
refreshing: boolean,
}
class NetworkErrorComponent extends React.PureComponent<Props, State> {
colors: Object;
state = {
refreshing: false,
};
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
render() {
return (
<View style={styles.outer}>
<View style={styles.inner}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={this.props.icon}
size={150}
color={this.colors.textDisabled}/>
</View>
<Subheading style={{
...styles.subheading,
color: this.colors.textDisabled
}}>
{this.props.message}
</Subheading>
<Button
mode={'contained'}
icon={'refresh'}
onPress={this.props.onRefresh}
style={styles.button}
>
{i18n.t("general.retry")}
</Button>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
outer: {
flex: 1,
},
inner: {
marginTop: 'auto',
marginBottom: 'auto',
},
iconContainer: {
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20
},
subheading: {
textAlign: 'center',
},
button: {
marginTop: 10,
marginLeft: 'auto',
marginRight: 'auto',
}
});
export default withTheme(NetworkErrorComponent);

View file

@ -1,38 +1,36 @@
// @flow
import * as React from 'react';
import {Dimensions, FlatList, Image, Platform, StyleSheet, View,} from 'react-native';
import {Dimensions, FlatList, Image, Platform, StyleSheet, View} from 'react-native';
import i18n from "i18n-js";
import {openBrowser} from "../utils/WebBrowser";
import * as WebBrowser from 'expo-web-browser';
import SidebarDivider from "./SidebarDivider";
import SidebarItem from "./SidebarItem";
import {TouchableRipple, withTheme} from "react-native-paper";
import ConnectionManager from "../managers/ConnectionManager";
import LogoutDialog from "./LogoutDialog";
import {TouchableRipple} from "react-native-paper";
const deviceWidth = Dimensions.get("window").width;
type Props = {
navigation: Object,
state: Object,
theme: Object,
};
type State = {
active: string,
isLoggedIn: boolean,
dialogVisible: boolean,
};
/**
* Component used to render the drawer menu content
*/
class SideBar extends React.PureComponent<Props, State> {
export default class SideBar extends React.PureComponent<Props, State> {
dataSet: Array<Object>;
state = {
active: 'Home',
};
getRenderItem: Function;
colors: Object;
/**
* Generate the dataset
@ -48,29 +46,6 @@ class SideBar extends React.PureComponent<Props, State> {
route: "Main",
icon: "home",
},
{
name: i18n.t('sidenav.divider4'),
route: "Divider4"
},
{
name: i18n.t('screens.login'),
route: "LoginScreen",
icon: "login",
onlyWhenLoggedOut: true,
},
{
name: i18n.t('screens.profile'),
route: "ProfileScreen",
icon: "account",
onlyWhenLoggedIn: true,
},
{
name: i18n.t('screens.logout'),
route: 'disconnect',
action: this.showDisconnectDialog,
icon: "logout",
onlyWhenLoggedIn: true,
},
{
name: i18n.t('sidenav.divider2'),
route: "Divider2"
@ -146,23 +121,6 @@ class SideBar extends React.PureComponent<Props, State> {
},
];
this.getRenderItem = this.getRenderItem.bind(this);
this.colors = props.theme.colors;
ConnectionManager.getInstance().setLoginCallback((value) => this.onLoginStateChange(value));
this.state = {
active: 'Home',
isLoggedIn: false,
dialogVisible: false,
};
ConnectionManager.getInstance().isLoggedIn().then(data => undefined).catch(error => undefined);
}
showDisconnectDialog = () => this.setState({ dialogVisible: true });
hideDisconnectDialog = () => this.setState({ dialogVisible: false });
onLoginStateChange(isLoggedIn: boolean) {
this.setState({isLoggedIn: isLoggedIn});
}
/**
@ -172,12 +130,10 @@ class SideBar extends React.PureComponent<Props, State> {
* @param item The item pressed
*/
onListItemPress(item: Object) {
if (item.link !== undefined)
openBrowser(item.link, this.colors.primary);
else if (item.action !== undefined)
item.action();
else
if (item.link === undefined)
this.props.navigation.navigate(item.route);
else
WebBrowser.openBrowserAsync(item.link);
}
/**
@ -198,11 +154,7 @@ class SideBar extends React.PureComponent<Props, State> {
*/
getRenderItem({item}: Object) {
const onListItemPress = this.onListItemPress.bind(this, item);
const onlyWhenLoggedOut = item.onlyWhenLoggedOut !== undefined && item.onlyWhenLoggedOut === true;
const onlyWhenLoggedIn = item.onlyWhenLoggedIn !== undefined && item.onlyWhenLoggedIn === true;
if (onlyWhenLoggedIn && !this.state.isLoggedIn || onlyWhenLoggedOut && this.state.isLoggedIn)
return null;
else if (item.icon !== undefined) {
if (item.icon !== undefined) {
return (
<SidebarItem
title={item.name}
@ -236,11 +188,6 @@ class SideBar extends React.PureComponent<Props, State> {
keyExtractor={this.listKeyExtractor}
renderItem={this.getRenderItem}
/>
<LogoutDialog
{...this.props}
visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
);
}
@ -266,5 +213,3 @@ const styles = StyleSheet.create({
marginTop: Platform.OS === "android" ? -3 : undefined
}
});
export default withTheme(SideBar);

View file

@ -1,197 +0,0 @@
// @flow
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,
};
const AUTH_URL = "https://www.amicale-insat.fr/api/password";
export default class ConnectionManager {
static instance: ConnectionManager | null = null;
#email: string;
#token: string;
loginCallback: Function;
/**
* Get this class instance or create one if none is found
* @returns {ConnectionManager}
*/
static getInstance(): ConnectionManager {
return ConnectionManager.instance === null ?
ConnectionManager.instance = new ConnectionManager() :
ConnectionManager.instance;
}
getToken() {
return this.#token;
}
onLoginStateChange(newState: boolean) {
this.loginCallback(newState);
}
setLoginCallback(callback: Function) {
this.loginCallback = callback;
}
async recoverLogin() {
return new Promise((resolve, reject) => {
if (this.#token !== undefined)
resolve(this.#token);
else {
SecureStore.getItemAsync('token')
.then((token) => {
if (token !== null) {
this.onLoginStateChange(true);
resolve(token);
} else
reject(false);
})
.catch(error => {
reject(false);
});
}
});
}
async isLoggedIn() {
return new Promise((resolve, reject) => {
this.recoverLogin()
.then(() => {
resolve(true);
})
.catch(() => {
reject(false);
})
});
}
async saveLogin(email: string, token: string) {
return new Promise((resolve, reject) => {
SecureStore.setItemAsync('token', token)
.then(() => {
this.#token = token;
this.#email = email;
this.onLoginStateChange(true);
resolve(true);
})
.catch(error => {
reject(false);
});
});
}
async disconnect() {
return new Promise((resolve, reject) => {
SecureStore.deleteItemAsync('token')
.then(() => {
this.onLoginStateChange(false);
resolve(true);
})
.catch((error) => {
reject(false);
});
});
}
async connect(email: string, password: string) {
let data = {
email: email,
password: password,
};
return new Promise((resolve, reject) => {
fetch(AUTH_URL, {
method: 'POST',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
body: JSON.stringify(data)
}).then(async (response) => response.json())
.then((data) => {
if (this.isConnectionResponseValid(data)) {
if (data.state) {
this.saveLogin(email, data.token)
.then(() => {
resolve(true);
})
.catch(() => {
reject(ERROR_TYPE.SAVE_TOKEN);
});
} else if (data.data.consent !== undefined && !data.data.consent)
reject(ERROR_TYPE.NO_CONSENT);
else
reject(ERROR_TYPE.BAD_CREDENTIALS);
} else
reject(ERROR_TYPE.CONNECTION_ERROR);
})
.catch((error) => {
reject(ERROR_TYPE.CONNECTION_ERROR);
});
});
}
isRequestResponseValid(response: Object) {
let valid = response !== undefined
&& response.state !== undefined
&& typeof response.state === "boolean";
if (valid && response.state)
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";
if (valid && response.state)
valid = valid
&& response.token !== undefined
&& response.token !== ''
&& typeof response.token === "string";
return valid;
}
async authenticatedRequest(url: string) {
return new Promise((resolve, reject) => {
this.recoverLogin()
.then(token => {
fetch(url, {
method: 'POST',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
body: JSON.stringify({token: token})
}).then(async (response) => response.json())
.then((data) => {
if (this.isRequestResponseValid(data)) {
if (data.state)
resolve(data.data);
else
reject(ERROR_TYPE.BAD_CREDENTIALS);
} else
reject(ERROR_TYPE.CONNECTION_ERROR);
})
.catch(() => {
reject(ERROR_TYPE.CONNECTION_ERROR);
});
})
.catch(() => {
reject(ERROR_TYPE.NO_TOKEN);
});
});
}
}

View file

@ -15,8 +15,6 @@ import Sidebar from "../components/Sidebar";
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
import HeaderButton from "../components/HeaderButton";
import i18n from "i18n-js";
import LoginScreen from "../screens/Amicale/LoginScreen";
import ProfileScreen from "../screens/Amicale/ProfileScreen";
const defaultScreenOptions = {
gestureEnabled: true,
@ -188,54 +186,6 @@ function TetrisStackComponent() {
);
}
const LoginStack = createStackNavigator();
function LoginStackComponent() {
return (
<LoginStack.Navigator
initialRouteName="LoginScreen"
headerMode="float"
screenOptions={defaultScreenOptions}
>
<LoginStack.Screen
name="LoginScreen"
component={LoginScreen}
options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.login'),
headerLeft: openDrawer
};
}}
/>
</LoginStack.Navigator>
);
}
const ProfileStack = createStackNavigator();
function ProfileStackComponent() {
return (
<ProfileStack.Navigator
initialRouteName="ProfileScreen"
headerMode="float"
screenOptions={defaultScreenOptions}
>
<ProfileStack.Screen
name="ProfileScreen"
component={ProfileScreen}
options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation);
return {
title: i18n.t('screens.profile'),
headerLeft: openDrawer
};
}}
/>
</ProfileStack.Navigator>
);
}
const Drawer = createDrawerNavigator();
function getDrawerContent(props) {
@ -281,14 +231,6 @@ export default function DrawerNavigator() {
name="TetrisScreen"
component={TetrisStackComponent}
/>
<Drawer.Screen
name="LoginScreen"
component={LoginStackComponent}
/>
<Drawer.Screen
name="ProfileScreen"
component={ProfileStackComponent}
/>
</Drawer.Navigator>
);
}

View file

@ -12,12 +12,7 @@
},
"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"
]
"setupFilesAfterEnv": ["jest-extended"]
},
"dependencies": {
"@expo/vector-icons": "~10.0.0",
@ -47,8 +42,7 @@
"react-native-screens": "2.0.0-alpha.12",
"react-native-webview": "7.4.3",
"react-native-appearance": "~0.3.1",
"expo-linear-gradient": "~8.0.0",
"expo-secure-store": "~8.0.0"
"expo-linear-gradient": "~8.0.0"
},
"devDependencies": {
"babel-preset-expo": "^8.0.0",

View file

@ -1,323 +0,0 @@
// @flow
import * as React from 'react';
import {Keyboard, KeyboardAvoidingView, ScrollView, StyleSheet, TouchableWithoutFeedback, View} from "react-native";
import {Avatar, Button, Card, HelperText, Text, TextInput, withTheme} from 'react-native-paper';
import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
import {openBrowser} from "../../utils/WebBrowser";
import i18n from 'i18n-js';
import AlertDialog from "../../components/AlertDialog";
type Props = {
navigation: Object,
}
type State = {
email: string,
password: string,
isEmailValidated: boolean,
isPasswordValidated: boolean,
loading: boolean,
dialogVisible: boolean,
dialogTitle: string,
dialogMessage: string,
}
const ICON_AMICALE = require('../../assets/amicale.png');
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,
dialogVisible: false,
dialogTitle: '',
dialogMessage: '',
};
colors: Object;
onEmailChange: Function;
onPasswordChange: Function;
validateEmail: Function;
validatePassword: Function;
onSubmit: Function;
onEmailSubmit: Function;
onResetPasswordClick: Function;
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;
}
showErrorDialog = (title: string, message: string) =>
this.setState({
dialogTitle: title,
dialogMessage: message,
dialogVisible: true
});
hideErrorDialog = () => this.setState({ dialogVisible: false });
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() && !this.state.loading;
}
onInputChange(isEmail: boolean, value: string) {
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
onEmailSubmit() {
this.passwordInputRef.focus();
}
onSubmit() {
if (this.shouldEnableLogin()) {
this.setState({loading: true});
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
.then((data) => {
this.handleSuccess();
})
.catch((error) => {
this.handleErrors(error);
})
.finally(() => {
this.setState({loading: false});
});
}
}
handleSuccess() {
this.props.navigation.navigate('ProfileScreen');
}
handleErrors(error: number) {
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;
default:
message = i18n.t("loginScreen.errors.unknown");
break;
}
this.showErrorDialog(title, message);
}
getFormInput() {
return (
<View>
<TextInput
label={i18n.t("loginScreen.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()}
>
{i18n.t("loginScreen.emailError")}
</HelperText>
<TextInput
ref={(ref) => {
this.passwordInputRef = ref;
}}
label={i18n.t("loginScreen.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()}
>
{i18n.t("loginScreen.passwordError")}
</HelperText>
</View>
);
}
getMainCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("loginScreen.title")}
subtitle={i18n.t("loginScreen.subtitle")}
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'}}>
{i18n.t("loginScreen.login")}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
getSecondaryCard() {
return (
<Card style={styles.card}>
<Card.Content>
<Text>{i18n.t("loginScreen.forgotPassword")}</Text>
<View style={styles.btnContainer}>
<Button
icon="reload"
mode="contained"
onPress={this.onResetPasswordClick}
style={{marginLeft: 'auto'}}>
{i18n.t("loginScreen.resetPassword")}
</Button>
</View>
<Text>{i18n.t("loginScreen.noAccount")}</Text>
</Card.Content>
</Card>
);
}
render() {
return (
<KeyboardAvoidingView
behavior={"height"}
contentContainerStyle={styles.container}
style={styles.container}
enabled
keyboardVerticalOffset={100}
>
<ScrollView>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View>
{this.getMainCard()}
{this.getSecondaryCard()}
</View>
</TouchableWithoutFeedback>
<AlertDialog
{...this.props}
visible={this.state.dialogVisible}
title={this.state.dialogTitle}
message={this.state.dialogMessage}
onDismiss={this.hideErrorDialog}
/>
</ScrollView>
</KeyboardAvoidingView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
card: {
margin: 10,
},
header: {
fontSize: 36,
marginBottom: 48
},
textInput: {},
btnContainer: {
marginTop: 5,
marginBottom: 10,
}
});
export default withTheme(LoginScreen);

View file

@ -1,255 +0,0 @@
// @flow
import * as React from 'react';
import {FlatList, StyleSheet, View} from "react-native";
import {Avatar, Button, Card, Divider, List, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../components/AuthenticatedScreen";
import {openBrowser} from "../../utils/WebBrowser";
import HeaderButton from "../../components/HeaderButton";
import i18n from 'i18n-js';
import LogoutDialog from "../../components/LogoutDialog";
type Props = {
navigation: Object,
theme: Object,
}
type State = {
dialogVisible: boolean,
}
class ProfileScreen extends React.Component<Props, State> {
state = {
dialogVisible: false,
};
colors: Object;
data: Object;
flatListData: Array<Object>;
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.flatListData = [
{id: '0'},
{id: '1'},
{id: '2'},
]
}
componentDidMount() {
const rightButton = this.getHeaderButtons.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
}
showDisconnectDialog = () => this.setState({ dialogVisible: true });
hideDisconnectDialog = () => this.setState({ dialogVisible: false });
getHeaderButtons() {
return <HeaderButton icon={'logout'} onPress={this.showDisconnectDialog}/>;
}
getScreen(data: Object) {
this.data = data;
return (
<View>
<FlatList
renderItem={item => this.getRenderItem(item)}
keyExtractor={item => item.id}
data={this.flatListData}
/>
<LogoutDialog
{...this.props}
visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
)
}
getRenderItem({item}: Object): any {
switch (item.id) {
case '0':
return this.getPersonalCard();
case '1':
return this.getClubCard();
case '2':
return this.getMembershipCar();
}
}
getPersonalCard() {
return (
<Card style={styles.card}>
<Card.Title
title={this.data.first_name + ' ' + this.data.last_name}
subtitle={this.data.email}
left={(props) => <Avatar.Icon
{...props}
icon="account"
color={this.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<Divider/>
<List.Section>
<List.Subheader>{i18n.t("profileScreen.personalInformation")}</List.Subheader>
{this.getPersonalListItem(this.data.birthday, "cake-variant")}
{this.getPersonalListItem(this.data.phone, "phone")}
{this.getPersonalListItem(this.data.email, "email")}
{this.getPersonalListItem(this.data.branch, "school")}
</List.Section>
<Divider/>
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => openBrowser(this.data.link, this.colors.primary)}
style={styles.editButton}>
{i18n.t("profileScreen.editInformation")}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
getClubCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("profileScreen.clubs")}
subtitle={i18n.t("profileScreen.clubsSubtitle")}
left={(props) => <Avatar.Icon
{...props}
icon="account-group"
color={this.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<Divider/>
{this.getClubList(this.data.clubs)}
</Card.Content>
</Card>
);
}
getMembershipCar() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("profileScreen.membership")}
subtitle={i18n.t("profileScreen.membershipSubtitle")}
left={(props) => <Avatar.Icon
{...props}
icon="credit-card"
color={this.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<List.Section>
{this.getMembershipItem(this.data.validity)}
</List.Section>
</Card.Content>
</Card>
);
}
getClubList(list: Array<string>) {
let dataset = [];
for (let i = 0; i < list.length; i++) {
dataset.push({name: list[i]});
}
return (
<FlatList
renderItem={({item}) =>
<List.Item
title={item.name}
left={props => <List.Icon {...props} icon="chevron-right"/>}
/>
}
keyExtractor={item => item.name}
data={dataset}
/>
);
}
getMembershipItem(state: boolean) {
return (
<List.Item
title={state ? i18n.t("profileScreen.membershipPayed") : i18n.t("profileScreen.membershipNotPayed")}
left={props => <List.Icon
{...props}
color={state ? this.colors.success : this.colors.danger}
icon={state ? 'check' : 'close'}
/>}
/>
);
}
isFieldAvailable(field: ?string) {
return field !== null;
}
getFieldValue(field: ?string) {
return this.isFieldAvailable(field)
? field
: i18n.t("profileScreen.noData");
}
getFieldColor(field: ?string) {
return this.isFieldAvailable(field)
? this.colors.text
: this.colors.textDisabled;
}
getPersonalListItem(field: ?string, icon: string) {
return (
<List.Item
title={this.getFieldValue(field)}
left={props => <List.Icon
{...props}
icon={icon}
color={this.getFieldColor(field)}
/>}
titleStyle={{color: this.getFieldColor(field)}}
/>
);
}
render() {
return (
<AuthenticatedScreen
{...this.props}
link={'https://www.amicale-insat.fr/api/user/profile'}
renderFunction={(data) => this.getScreen(data)}
/>
);
}
}
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
editButton: {
marginLeft: 'auto'
}
});
export default withTheme(ProfileScreen);

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() {
openBrowser("https://www.etud.insa-toulouse.fr/~tutorinsa/", this.colors.primary);
WebBrowser.openBrowserAsync("https://www.etud.insa-toulouse.fr/~tutorinsa/");
}
onProximoClick() {
@ -402,7 +402,7 @@ class HomeScreen extends React.Component<Props> {
}
openLink(link: string) {
openBrowser(link, this.colors.primary);
WebBrowser.openBrowserAsync(link);
}
/**

View file

@ -12,16 +12,12 @@
"bluemind": "INSA Mails",
"ent": "INSA ENT",
"about": "About",
"debug": "Debug",
"login": "Login",
"logout": "Logout",
"profile": "Profile"
"debug": "Debug"
},
"sidenav": {
"divider1": "Student websites",
"divider2": "Services",
"divider3": "Personalisation",
"divider4": "Amicale"
"divider3": "Personalisation"
},
"intro": {
"slide1": {
@ -211,50 +207,8 @@
"computerRoom": "Computer",
"bibRoom": "Bib'Box"
},
"profileScreen": {
"personalInformation": "Personal information",
"noData": "No data",
"editInformation": "Edit Information",
"clubs": "Clubs",
"clubsSubtitle": "Clubs you are part of",
"membership": "Membership Fee",
"membershipSubtitle": "Allows you to take part in various activities",
"membershipPayed": "Payed",
"membershipNotPayed": "not Payed"
},
"loginScreen": {
"title": "Amicale account",
"subtitle": "Please enter your credentials",
"email": "Email",
"emailError": "Please enter a valid email",
"password": "Password",
"passwordError": "Please enter a password",
"login": "Login",
"forgotPassword": "Forgot your password? Click on the button below to get a new one.",
"resetPassword": "Reset Password",
"noAccount": "No Account? Go to the Amicale's building during open hours to create one.",
"errors": {
"title": "Error!",
"connection": "Network error. Please check your internet connection.",
"credentials": "Email or password invalid.",
"saveToken": "Failed to save connection information, please contact support.",
"consent": "You did not give your consent for data processing to the Amicale.",
"unknown": "Unknown error, please contact support."
}
},
"dialog": {
"ok": "OK",
"yes": "Yes",
"cancel": "Cancel",
"disconnect": {
"title": "Disconnect",
"titleLoading": "Disconnecting...",
"message": "Are you sure you want to disconnect from your Amicale account?"
}
},
"general": {
"loading": "Loading...",
"retry": "Retry",
"networkError": "Unable to contact servers. Make sure you are connected to Internet."
},
"date": {

View file

@ -12,16 +12,12 @@
"bluemind": "Mails INSA",
"ent": "ENT INSA",
"about": "À Propos",
"debug": "Debug",
"login": "Se Connecter",
"logout": "Se Déconnecter",
"profile": "Profil"
"debug": "Debug"
},
"sidenav": {
"divider1": "Sites étudiants",
"divider2": "Services",
"divider3": "Personnalisation",
"divider4": "Amicale"
"divider3": "Personnalisation"
},
"intro": {
"slide1": {
@ -212,50 +208,8 @@
"computerRoom": "Ordi",
"bibRoom": "Bib'Box"
},
"profileScreen": {
"personalInformation": "Informations Personnelles",
"noData": "Pas de données",
"editInformation": "Modifier les informations",
"clubs": "Clubs",
"clubsSubtitle": "Liste de vos clubs",
"membership": "Cotisation",
"membershipSubtitle": "Permet de participer à diverses activités",
"membershipPayed": "Payée",
"membershipNotPayed": "Non payée"
},
"loginScreen": {
"title": "Compte Amicale",
"subtitle": "Entrez vos identifiants",
"email": "Email",
"emailError": "Merci d'entrer un email valide",
"password": "Mot de passe",
"passwordError": "Merci d'entrer un mot de passe",
"login": "Se Connecter",
"forgotPassword": "Mot de passe oublié ? Cliquez sur le bouton ci-dessous pour en créer un nouveau.",
"resetPassword": "Réinitialiser mot de passe",
"noAccount": "Pas de compte ? Passez à l'Amicale pendant une perm pour en créer un.",
"errors": {
"title": "Erreur !",
"connection": "Erreur de réseau. Merci de vérifier votre connexion Internet.",
"credentials": "Email ou mot de passe invalide.",
"saveToken": "Erreur de sauvegarde des informations de connexion, merci de contacter le support.",
"consent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
"unknown": "Erreur inconnue, merci de contacter le support."
}
},
"dialog": {
"ok": "OK",
"yes": "Oui",
"cancel": "Annuler",
"disconnect": {
"title": "Déconnexion",
"titleLoading": "Déconnexion...",
"message": "Voulez vous vraiment vous déconnecter de votre compte Amicale ??"
}
},
"general": {
"loading": "Chargement...",
"retry": "Réessayer",
"networkError": "Impossible de contacter les serveurs. Assurez-vous d'être connecté à internet."
},
"date": {

View file

@ -1,11 +0,0 @@
// @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,
});
}