Compare commits
24 commits
14856616df
...
2010170e8b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2010170e8b | |||
| 4bc6ae07b5 | |||
| 4ce6865b6a | |||
| a1f20fbf4e | |||
| ee13d099fe | |||
| f10242b187 | |||
| cff18d8256 | |||
| 5a788f4b6d | |||
| 5e40e271b7 | |||
| d7d6146245 | |||
| 1c473a1712 | |||
| b118c98e93 | |||
| 6b2aca3131 | |||
| 4406efaf41 | |||
| c5e79f45c3 | |||
| 2b0945fe5b | |||
| 4259dd779d | |||
| 5aa3afd383 | |||
| 0b19915a62 | |||
| 6b336cfd03 | |||
| e3b3657e6e | |||
| 5bf1e7586b | |||
| 4b370d5810 | |||
| fbabb4d7af |
15 changed files with 1656 additions and 21 deletions
292
__tests__/managers/ConnectionManager.test.js
Normal file
292
__tests__/managers/ConnectionManager.test.js
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
37
components/AlertDialog.js
Normal file
37
components/AlertDialog.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
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);
|
||||||
139
components/AuthenticatedScreen.js
Normal file
139
components/AuthenticatedScreen.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
// @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);
|
||||||
83
components/LogoutDialog.js
Normal file
83
components/LogoutDialog.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// @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);
|
||||||
87
components/NetworkErrorComponent.js
Normal file
87
components/NetworkErrorComponent.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// @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);
|
||||||
|
|
@ -1,36 +1,38 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
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 i18n from "i18n-js";
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import {openBrowser} from "../utils/WebBrowser";
|
||||||
import SidebarDivider from "./SidebarDivider";
|
import SidebarDivider from "./SidebarDivider";
|
||||||
import SidebarItem from "./SidebarItem";
|
import SidebarItem from "./SidebarItem";
|
||||||
import {TouchableRipple} from "react-native-paper";
|
import {TouchableRipple, withTheme} from "react-native-paper";
|
||||||
|
import ConnectionManager from "../managers/ConnectionManager";
|
||||||
|
import LogoutDialog from "./LogoutDialog";
|
||||||
|
|
||||||
const deviceWidth = Dimensions.get("window").width;
|
const deviceWidth = Dimensions.get("window").width;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
navigation: Object,
|
navigation: Object,
|
||||||
state: Object,
|
state: Object,
|
||||||
|
theme: Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
active: string,
|
active: string,
|
||||||
|
isLoggedIn: boolean,
|
||||||
|
dialogVisible: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to render the drawer menu content
|
* 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>;
|
dataSet: Array<Object>;
|
||||||
|
|
||||||
state = {
|
|
||||||
active: 'Home',
|
|
||||||
};
|
|
||||||
|
|
||||||
getRenderItem: Function;
|
getRenderItem: Function;
|
||||||
|
colors: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the dataset
|
* Generate the dataset
|
||||||
|
|
@ -46,6 +48,29 @@ export default class SideBar extends React.PureComponent<Props, State> {
|
||||||
route: "Main",
|
route: "Main",
|
||||||
icon: "home",
|
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'),
|
name: i18n.t('sidenav.divider2'),
|
||||||
route: "Divider2"
|
route: "Divider2"
|
||||||
|
|
@ -121,6 +146,23 @@ export default class SideBar extends React.PureComponent<Props, State> {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.getRenderItem = this.getRenderItem.bind(this);
|
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});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,10 +172,12 @@ export default class SideBar extends React.PureComponent<Props, State> {
|
||||||
* @param item The item pressed
|
* @param item The item pressed
|
||||||
*/
|
*/
|
||||||
onListItemPress(item: Object) {
|
onListItemPress(item: Object) {
|
||||||
if (item.link === undefined)
|
if (item.link !== undefined)
|
||||||
this.props.navigation.navigate(item.route);
|
openBrowser(item.link, this.colors.primary);
|
||||||
|
else if (item.action !== undefined)
|
||||||
|
item.action();
|
||||||
else
|
else
|
||||||
WebBrowser.openBrowserAsync(item.link);
|
this.props.navigation.navigate(item.route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,7 +198,11 @@ export default class SideBar extends React.PureComponent<Props, State> {
|
||||||
*/
|
*/
|
||||||
getRenderItem({item}: Object) {
|
getRenderItem({item}: Object) {
|
||||||
const onListItemPress = this.onListItemPress.bind(this, item);
|
const onListItemPress = this.onListItemPress.bind(this, item);
|
||||||
if (item.icon !== undefined) {
|
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) {
|
||||||
return (
|
return (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
title={item.name}
|
title={item.name}
|
||||||
|
|
@ -188,6 +236,11 @@ export default class SideBar extends React.PureComponent<Props, State> {
|
||||||
keyExtractor={this.listKeyExtractor}
|
keyExtractor={this.listKeyExtractor}
|
||||||
renderItem={this.getRenderItem}
|
renderItem={this.getRenderItem}
|
||||||
/>
|
/>
|
||||||
|
<LogoutDialog
|
||||||
|
{...this.props}
|
||||||
|
visible={this.state.dialogVisible}
|
||||||
|
onDismiss={this.hideDisconnectDialog}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -213,3 +266,5 @@ const styles = StyleSheet.create({
|
||||||
marginTop: Platform.OS === "android" ? -3 : undefined
|
marginTop: Platform.OS === "android" ? -3 : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default withTheme(SideBar);
|
||||||
|
|
|
||||||
197
managers/ConnectionManager.js
Normal file
197
managers/ConnectionManager.js
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
// @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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@ import Sidebar from "../components/Sidebar";
|
||||||
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
|
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
|
||||||
import HeaderButton from "../components/HeaderButton";
|
import HeaderButton from "../components/HeaderButton";
|
||||||
import i18n from "i18n-js";
|
import i18n from "i18n-js";
|
||||||
|
import LoginScreen from "../screens/Amicale/LoginScreen";
|
||||||
|
import ProfileScreen from "../screens/Amicale/ProfileScreen";
|
||||||
|
|
||||||
const defaultScreenOptions = {
|
const defaultScreenOptions = {
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
|
|
@ -186,6 +188,54 @@ 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();
|
const Drawer = createDrawerNavigator();
|
||||||
|
|
||||||
function getDrawerContent(props) {
|
function getDrawerContent(props) {
|
||||||
|
|
@ -231,6 +281,14 @@ export default function DrawerNavigator() {
|
||||||
name="TetrisScreen"
|
name="TetrisScreen"
|
||||||
component={TetrisStackComponent}
|
component={TetrisStackComponent}
|
||||||
/>
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="LoginScreen"
|
||||||
|
component={LoginStackComponent}
|
||||||
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="ProfileScreen"
|
||||||
|
component={ProfileStackComponent}
|
||||||
|
/>
|
||||||
</Drawer.Navigator>
|
</Drawer.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -12,7 +12,12 @@
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "react-native",
|
"preset": "react-native",
|
||||||
"setupFilesAfterEnv": ["jest-extended"]
|
"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"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "~10.0.0",
|
"@expo/vector-icons": "~10.0.0",
|
||||||
|
|
@ -42,7 +47,8 @@
|
||||||
"react-native-screens": "2.0.0-alpha.12",
|
"react-native-screens": "2.0.0-alpha.12",
|
||||||
"react-native-webview": "7.4.3",
|
"react-native-webview": "7.4.3",
|
||||||
"react-native-appearance": "~0.3.1",
|
"react-native-appearance": "~0.3.1",
|
||||||
"expo-linear-gradient": "~8.0.0"
|
"expo-linear-gradient": "~8.0.0",
|
||||||
|
"expo-secure-store": "~8.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-preset-expo": "^8.0.0",
|
"babel-preset-expo": "^8.0.0",
|
||||||
|
|
|
||||||
323
screens/Amicale/LoginScreen.js
Normal file
323
screens/Amicale/LoginScreen.js
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
// @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);
|
||||||
255
screens/Amicale/ProfileScreen.js
Normal file
255
screens/Amicale/ProfileScreen.js
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
// @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);
|
||||||
|
|
@ -4,13 +4,13 @@ import * as React from 'react';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
import i18n from "i18n-js";
|
import i18n from "i18n-js";
|
||||||
import DashboardItem from "../components/EventDashboardItem";
|
import DashboardItem from "../components/EventDashboardItem";
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
|
||||||
import WebSectionList from "../components/WebSectionList";
|
import WebSectionList from "../components/WebSectionList";
|
||||||
import {Text, withTheme} from 'react-native-paper';
|
import {Text, withTheme} from 'react-native-paper';
|
||||||
import FeedItem from "../components/FeedItem";
|
import FeedItem from "../components/FeedItem";
|
||||||
import SquareDashboardItem from "../components/SquareDashboardItem";
|
import SquareDashboardItem from "../components/SquareDashboardItem";
|
||||||
import PreviewEventDashboardItem from "../components/PreviewEventDashboardItem";
|
import PreviewEventDashboardItem from "../components/PreviewEventDashboardItem";
|
||||||
import {stringToDate} from "../utils/Planning";
|
import {stringToDate} from "../utils/Planning";
|
||||||
|
import {openBrowser} from "../utils/WebBrowser";
|
||||||
// import DATA from "../dashboard_data.json";
|
// import DATA from "../dashboard_data.json";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ class HomeScreen extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
onTutorInsaClick() {
|
onTutorInsaClick() {
|
||||||
WebBrowser.openBrowserAsync("https://www.etud.insa-toulouse.fr/~tutorinsa/");
|
openBrowser("https://www.etud.insa-toulouse.fr/~tutorinsa/", this.colors.primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
onProximoClick() {
|
onProximoClick() {
|
||||||
|
|
@ -402,7 +402,7 @@ class HomeScreen extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
openLink(link: string) {
|
openLink(link: string) {
|
||||||
WebBrowser.openBrowserAsync(link);
|
openBrowser(link, this.colors.primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,16 @@
|
||||||
"bluemind": "INSA Mails",
|
"bluemind": "INSA Mails",
|
||||||
"ent": "INSA ENT",
|
"ent": "INSA ENT",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"debug": "Debug"
|
"debug": "Debug",
|
||||||
|
"login": "Login",
|
||||||
|
"logout": "Logout",
|
||||||
|
"profile": "Profile"
|
||||||
},
|
},
|
||||||
"sidenav": {
|
"sidenav": {
|
||||||
"divider1": "Student websites",
|
"divider1": "Student websites",
|
||||||
"divider2": "Services",
|
"divider2": "Services",
|
||||||
"divider3": "Personalisation"
|
"divider3": "Personalisation",
|
||||||
|
"divider4": "Amicale"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"slide1": {
|
"slide1": {
|
||||||
|
|
@ -207,8 +211,50 @@
|
||||||
"computerRoom": "Computer",
|
"computerRoom": "Computer",
|
||||||
"bibRoom": "Bib'Box"
|
"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": {
|
"general": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
|
"retry": "Retry",
|
||||||
"networkError": "Unable to contact servers. Make sure you are connected to Internet."
|
"networkError": "Unable to contact servers. Make sure you are connected to Internet."
|
||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,16 @@
|
||||||
"bluemind": "Mails INSA",
|
"bluemind": "Mails INSA",
|
||||||
"ent": "ENT INSA",
|
"ent": "ENT INSA",
|
||||||
"about": "À Propos",
|
"about": "À Propos",
|
||||||
"debug": "Debug"
|
"debug": "Debug",
|
||||||
|
"login": "Se Connecter",
|
||||||
|
"logout": "Se Déconnecter",
|
||||||
|
"profile": "Profil"
|
||||||
},
|
},
|
||||||
"sidenav": {
|
"sidenav": {
|
||||||
"divider1": "Sites étudiants",
|
"divider1": "Sites étudiants",
|
||||||
"divider2": "Services",
|
"divider2": "Services",
|
||||||
"divider3": "Personnalisation"
|
"divider3": "Personnalisation",
|
||||||
|
"divider4": "Amicale"
|
||||||
},
|
},
|
||||||
"intro": {
|
"intro": {
|
||||||
"slide1": {
|
"slide1": {
|
||||||
|
|
@ -208,8 +212,50 @@
|
||||||
"computerRoom": "Ordi",
|
"computerRoom": "Ordi",
|
||||||
"bibRoom": "Bib'Box"
|
"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": {
|
"general": {
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
|
"retry": "Réessayer",
|
||||||
"networkError": "Impossible de contacter les serveurs. Assurez-vous d'être connecté à internet."
|
"networkError": "Impossible de contacter les serveurs. Assurez-vous d'être connecté à internet."
|
||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
|
|
|
||||||
11
utils/WebBrowser.js
Normal file
11
utils/WebBrowser.js
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue