Compare commits

..

No commits in common. "8723765e43f3a0699b341b131df9d214b5757a89" and "1ede8f4e9ad7263dc14c0dc8d4905f5c5474c6c0" have entirely different histories.

24 changed files with 333 additions and 314 deletions

5
App.js
View file

@ -14,7 +14,6 @@ import {initExpoToken} from "./utils/Notifications";
import {Provider as PaperProvider} from 'react-native-paper'; import {Provider as PaperProvider} from 'react-native-paper';
import AprilFoolsManager from "./managers/AprilFoolsManager"; import AprilFoolsManager from "./managers/AprilFoolsManager";
import Update from "./constants/Update"; import Update from "./constants/Update";
import ConnectionManager from "./managers/ConnectionManager";
type Props = {}; type Props = {};
@ -92,10 +91,6 @@ export default class App extends React.Component<Props, State> {
await AsyncStorageManager.getInstance().loadPreferences(); await AsyncStorageManager.getInstance().loadPreferences();
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme); ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
await initExpoToken(); await initExpoToken();
try {
await ConnectionManager.getInstance().recoverLogin();
} catch (e) {}
this.onLoadFinished(); this.onLoadFinished();
} }

View file

@ -11,19 +11,20 @@ afterEach(() => {
}); });
test('isLoggedIn yes', () => { test('isLoggedIn yes', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return 'token'; return Promise.resolve(true);
}); });
return expect(c.isLoggedIn()).toBe(true); return expect(c.isLoggedIn()).resolves.toBe(true);
}); });
test('isLoggedIn no', () => { test('isLoggedIn no', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return null; return Promise.reject(false);
}); });
return expect(c.isLoggedIn()).toBe(false); return expect(c.isLoggedIn()).rejects.toBe(false);
}); });
test('recoverLogin error crypto', () => { test('recoverLogin error crypto', () => {
jest.spyOn(SecureStore, 'getItemAsync').mockImplementationOnce(() => { jest.spyOn(SecureStore, 'getItemAsync').mockImplementationOnce(() => {
return Promise.reject(); return Promise.reject();
@ -110,12 +111,7 @@ test("isConnectionResponseValid", () => {
state: false, state: false,
}; };
expect(c.isConnectionResponseValid(json)).toBeTrue(); expect(c.isConnectionResponseValid(json)).toBeTrue();
json = {
state: false,
message: 'Adresse mail ou mot de passe incorrect',
token: ''
};
expect(c.isConnectionResponseValid(json)).toBeTrue();
json = { json = {
state: true, state: true,
message: 'Connexion confirmée', message: 'Connexion confirmée',
@ -147,6 +143,7 @@ test("isConnectionResponseValid", () => {
expect(c.isConnectionResponseValid(json)).toBeFalse(); expect(c.isConnectionResponseValid(json)).toBeFalse();
}); });
test("connect bad credentials", () => { test("connect bad credentials", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
@ -181,25 +178,6 @@ test("connect good credentials", () => {
return expect(c.connect('email', 'password')).resolves.toBeTruthy(); return expect(c.connect('email', 'password')).resolves.toBeTruthy();
}); });
test("connect good credentials no consent", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: false,
message: 'pas de consent',
token: '',
data: {
consent: false,
}
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.NO_CONSENT);
});
test("connect good credentials, fail save token", () => { test("connect good credentials, fail save token", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
@ -243,8 +221,8 @@ test("connect bogus response 1", () => {
test("authenticatedRequest success", () => { test("authenticatedRequest success", () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return 'token'; return Promise.resolve('token');
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
@ -258,8 +236,8 @@ test("authenticatedRequest success", () => {
}); });
test("authenticatedRequest error wrong token", () => { test("authenticatedRequest error wrong token", () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return 'token'; return Promise.resolve('token');
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
@ -273,8 +251,8 @@ test("authenticatedRequest error wrong token", () => {
}); });
test("authenticatedRequest error bogus response", () => { test("authenticatedRequest error bogus response", () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return 'token'; return Promise.resolve('token');
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
@ -288,8 +266,8 @@ test("authenticatedRequest error bogus response", () => {
}); });
test("authenticatedRequest connection error", () => { test("authenticatedRequest connection error", () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return 'token'; return Promise.resolve('token');
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject() return Promise.reject()
@ -299,8 +277,8 @@ test("authenticatedRequest connection error", () => {
}); });
test("authenticatedRequest error no token", () => { test("authenticatedRequest error no token", () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return null; return Promise.reject(false);
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({

View file

@ -24,7 +24,7 @@ class AuthenticatedScreen extends React.Component<Props, State> {
loading: true, loading: true,
}; };
currentUserToken: string | null; currentUserToken: string;
connectionManager: ConnectionManager; connectionManager: ConnectionManager;
errorCode: number; errorCode: number;
data: Object; data: Object;
@ -35,6 +35,8 @@ class AuthenticatedScreen extends React.Component<Props, State> {
this.colors = props.theme.colors; this.colors = props.theme.colors;
this.connectionManager = ConnectionManager.getInstance(); this.connectionManager = ConnectionManager.getInstance();
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this)); this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
this.fetchData();
} }
onScreenFocus() { onScreenFocus() {
@ -45,7 +47,8 @@ class AuthenticatedScreen extends React.Component<Props, State> {
fetchData = () => { fetchData = () => {
if (!this.state.loading) if (!this.state.loading)
this.setState({loading: true}); this.setState({loading: true});
if (this.connectionManager.isLoggedIn()) { this.connectionManager.isLoggedIn()
.then(() => {
this.connectionManager.authenticatedRequest(this.props.link) this.connectionManager.authenticatedRequest(this.props.link)
.then((data) => { .then((data) => {
this.onFinishedLoading(data, -1); this.onFinishedLoading(data, -1);
@ -53,16 +56,17 @@ class AuthenticatedScreen extends React.Component<Props, State> {
.catch((error) => { .catch((error) => {
this.onFinishedLoading(undefined, error); this.onFinishedLoading(undefined, error);
}); });
} else { })
.catch((error) => {
this.onFinishedLoading(undefined, ERROR_TYPE.BAD_CREDENTIALS); this.onFinishedLoading(undefined, ERROR_TYPE.BAD_CREDENTIALS);
} });
}; };
onFinishedLoading(data: Object, error: number) { onFinishedLoading(data: Object, error: number) {
this.data = data; this.data = data;
this.currentUserToken = data !== undefined this.currentUserToken = data !== undefined
? this.connectionManager.getToken() ? this.connectionManager.getToken()
: null; : '';
this.errorCode = error; this.errorCode = error;
this.setState({loading: false}); this.setState({loading: false});
} }

View file

@ -13,7 +13,7 @@ function HeaderButton(props) {
<IconButton <IconButton
icon={props.icon} icon={props.icon}
size={26} size={26}
color={props.color !== undefined ? props.color : colors.text} color={colors.text}
onPress={props.onPress} onPress={props.onPress}
/> />
); );

View file

@ -3,7 +3,6 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Card, withTheme} from 'react-native-paper'; import {Button, Card, withTheme} from 'react-native-paper';
import {StyleSheet} from "react-native"; import {StyleSheet} from "react-native";
import i18n from 'i18n-js';
type Props = { type Props = {
navigation: Object, navigation: Object,
@ -21,8 +20,6 @@ class ActionsDashBoardItem extends React.PureComponent<Props> {
openDrawer = () => this.props.navigation.openDrawer(); openDrawer = () => this.props.navigation.openDrawer();
gotToSettings = () => this.props.navigation.navigate("SettingsScreen");
render() { render() {
return ( return (
<Card style={{ <Card style={{
@ -34,17 +31,10 @@ class ActionsDashBoardItem extends React.PureComponent<Props> {
icon="information" icon="information"
mode="contained" mode="contained"
onPress={this.openDrawer} onPress={this.openDrawer}
style={styles.servicesButton} style={styles.button}
> >
{i18n.t("homeScreen.servicesButton")} PLUS DE SERVICES
</Button> </Button>
<Button
icon="settings"
mode="contained"
onPress={this.gotToSettings}
style={styles.settingsButton}
compact
/>
</Card.Content> </Card.Content>
</Card> </Card>
); );
@ -68,12 +58,8 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
}, },
servicesButton: { button: {
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 5,
},
settingsButton: {
marginLeft: 5,
marginRight: 'auto', marginRight: 'auto',
} }
}); });

View file

@ -1,40 +0,0 @@
// @flow
import * as React from 'react';
import {Avatar, List, Text, withTheme} from 'react-native-paper';
import i18n from "i18n-js";
type Props = {
onPress: Function,
color: string,
item: Object,
}
class ProximoListItem extends React.PureComponent<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
render() {
return (
<List.Item
title={this.props.item.name}
description={this.props.item.quantity + ' ' + i18n.t('proximoScreen.inStock')}
descriptionStyle={{color: this.props.color}}
onPress={this.props.onPress}
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64}
source={{uri: this.props.item.image}}/>}
right={() =>
<Text style={{fontWeight: "bold"}}>
{this.props.item.price}
</Text>}
/>
);
}
}
export default withTheme(ProximoListItem);

View file

@ -0,0 +1,33 @@
import * as React from 'react';
import {FlatList} from "react-native";
type Props = {
data: Array<Object>,
keyExtractor: Function,
renderItem: Function,
updateData: number,
}
/**
* FlatList implementing PureComponent for increased performance.
*
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData.
*/
export default class PureFlatList extends React.PureComponent<Props> {
static defaultProps = {
updateData: null,
};
render() {
return (
<FlatList
data={this.props.data}
keyExtractor={this.props.keyExtractor}
style={{minHeight: 300, width: '100%'}}
renderItem={this.props.renderItem}
/>
);
}
}

View file

@ -57,6 +57,8 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
onFetchSuccess: Function; onFetchSuccess: Function;
onFetchError: Function; onFetchError: Function;
getEmptySectionHeader: Function; getEmptySectionHeader: Function;
showSnackBar: Function;
hideSnackBar: Function;
constructor() { constructor() {
super(); super();
@ -65,6 +67,8 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
this.onFetchSuccess = this.onFetchSuccess.bind(this); this.onFetchSuccess = this.onFetchSuccess.bind(this);
this.onFetchError = this.onFetchError.bind(this); this.onFetchError = this.onFetchError.bind(this);
this.getEmptySectionHeader = this.getEmptySectionHeader.bind(this); this.getEmptySectionHeader = this.getEmptySectionHeader.bind(this);
this.showSnackBar = this.showSnackBar.bind(this);
this.hideSnackBar = this.hideSnackBar.bind(this);
} }
/** /**
@ -155,12 +159,16 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
/** /**
* Shows the error popup * Shows the error popup
*/ */
showSnackBar = () => this.setState({snackbarVisible: true}); showSnackBar() {
this.setState({snackbarVisible: true})
}
/** /**
* Hides the error popup * Hides the error popup
*/ */
hideSnackBar = () => this.setState({snackbarVisible: false}); hideSnackBar() {
this.setState({snackbarVisible: false})
}
render() { render() {
let dataset = []; let dataset = [];
@ -169,10 +177,20 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
const shouldRenderHeader = this.props.renderSectionHeader !== null; const shouldRenderHeader = this.props.renderSectionHeader !== null;
return ( return (
<View> <View>
<Snackbar
visible={this.state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: this.hideSnackBar,
}}
duration={4000}
>
{i18n.t("homeScreen.listUpdateFail")}
</Snackbar>
{/*$FlowFixMe*/} {/*$FlowFixMe*/}
<SectionList <SectionList
sections={dataset} sections={dataset}
extraData={this.props.updateData}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={this.state.refreshing} refreshing={this.state.refreshing}
@ -194,17 +212,6 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
onRefresh={this.onRefresh}/> onRefresh={this.onRefresh}/>
} }
/> />
<Snackbar
visible={this.state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: () => {},
}}
duration={4000}
>
{i18n.t("homeScreen.listUpdateFail")}
</Snackbar>
</View> </View>
); );
} }

View file

@ -19,6 +19,7 @@ type Props = {
}; };
type State = { type State = {
active: string,
isLoggedIn: boolean, isLoggedIn: boolean,
dialogVisible: boolean, dialogVisible: boolean,
}; };
@ -153,11 +154,13 @@ class SideBar extends React.PureComponent<Props, State> {
]; ];
this.getRenderItem = this.getRenderItem.bind(this); this.getRenderItem = this.getRenderItem.bind(this);
this.colors = props.theme.colors; this.colors = props.theme.colors;
ConnectionManager.getInstance().addLoginStateListener((value) => this.onLoginStateChange(value)); ConnectionManager.getInstance().setLoginCallback((value) => this.onLoginStateChange(value));
this.state = { this.state = {
isLoggedIn: ConnectionManager.getInstance().isLoggedIn(), active: 'Home',
isLoggedIn: false,
dialogVisible: false, dialogVisible: false,
}; };
ConnectionManager.getInstance().isLoggedIn().then(data => undefined).catch(error => undefined);
} }
showDisconnectDialog = () => this.setState({ dialogVisible: true }); showDisconnectDialog = () => this.setState({ dialogVisible: true });
@ -236,10 +239,9 @@ class SideBar extends React.PureComponent<Props, State> {
style={styles.drawerCover} style={styles.drawerCover}
/> />
</TouchableRipple> </TouchableRipple>
{/*$FlowFixMe*/}
<FlatList <FlatList
data={this.dataSet} data={this.dataSet}
extraData={this.state.isLoggedIn} extraData={this.state}
keyExtractor={this.listKeyExtractor} keyExtractor={this.listKeyExtractor}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
/> />

View file

@ -16,14 +16,9 @@ export default class ConnectionManager {
static instance: ConnectionManager | null = null; static instance: ConnectionManager | null = null;
#email: string; #email: string;
#token: string | null; #token: string;
listeners: Array<Function>; loginCallback: Function;
constructor() {
this.#token = null;
this.listeners = [];
}
/** /**
* Get this class instance or create one if none is found * Get this class instance or create one if none is found
@ -40,24 +35,20 @@ export default class ConnectionManager {
} }
onLoginStateChange(newState: boolean) { onLoginStateChange(newState: boolean) {
for (let i = 0; i < this.listeners.length; i++) { this.loginCallback(newState);
if (this.listeners[i] !== undefined)
this.listeners[i](newState);
}
} }
addLoginStateListener(listener: Function) { setLoginCallback(callback: Function) {
this.listeners.push(listener); this.loginCallback = callback;
} }
async recoverLogin() { async recoverLogin() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.getToken() !== null) if (this.#token !== undefined)
resolve(this.getToken()); resolve(this.#token);
else { else {
SecureStore.getItemAsync('token') SecureStore.getItemAsync('token')
.then((token) => { .then((token) => {
this.#token = token;
if (token !== null) { if (token !== null) {
this.onLoginStateChange(true); this.onLoginStateChange(true);
resolve(token); resolve(token);
@ -71,8 +62,16 @@ export default class ConnectionManager {
}); });
} }
isLoggedIn() { async isLoggedIn() {
return this.getToken() !== null; return new Promise((resolve, reject) => {
this.recoverLogin()
.then(() => {
resolve(true);
})
.catch(() => {
reject(false);
})
});
} }
async saveLogin(email: string, token: string) { async saveLogin(email: string, token: string) {
@ -94,7 +93,6 @@ export default class ConnectionManager {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
SecureStore.deleteItemAsync('token') SecureStore.deleteItemAsync('token')
.then(() => { .then(() => {
this.#token = null;
this.onLoginStateChange(false); this.onLoginStateChange(false);
resolve(true); resolve(true);
}) })
@ -128,9 +126,7 @@ export default class ConnectionManager {
.catch(() => { .catch(() => {
reject(ERROR_TYPE.SAVE_TOKEN); reject(ERROR_TYPE.SAVE_TOKEN);
}); });
} else if (data.data !== undefined } else if (data.data.consent !== undefined && !data.data.consent)
&& data.data.consent !== undefined
&& !data.data.consent)
reject(ERROR_TYPE.NO_CONSENT); reject(ERROR_TYPE.NO_CONSENT);
else else
reject(ERROR_TYPE.BAD_CREDENTIALS); reject(ERROR_TYPE.BAD_CREDENTIALS);
@ -170,14 +166,15 @@ export default class ConnectionManager {
async authenticatedRequest(url: string) { async authenticatedRequest(url: string) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.getToken() !== null) { this.recoverLogin()
.then(token => {
fetch(url, { fetch(url, {
method: 'POST', method: 'POST',
headers: new Headers({ headers: new Headers({
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}), }),
body: JSON.stringify({token: this.getToken()}) body: JSON.stringify({token: token})
}).then(async (response) => response.json()) }).then(async (response) => response.json())
.then((data) => { .then((data) => {
if (this.isRequestResponseValid(data)) { if (this.isRequestResponseValid(data)) {
@ -191,8 +188,10 @@ export default class ConnectionManager {
.catch(() => { .catch(() => {
reject(ERROR_TYPE.CONNECTION_ERROR); reject(ERROR_TYPE.CONNECTION_ERROR);
}); });
} else })
.catch(() => {
reject(ERROR_TYPE.NO_TOKEN); reject(ERROR_TYPE.NO_TOKEN);
}); });
});
} }
} }

View file

@ -244,7 +244,9 @@ class AboutScreen extends React.Component<Props, State> {
<Card.Content> <Card.Content>
<FlatList <FlatList
data={this.appData} data={this.appData}
extraData={this.state}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
listKey={"app"}
renderItem={this.getCardItem} renderItem={this.getCardItem}
/> />
</Card.Content> </Card.Content>
@ -267,15 +269,17 @@ class AboutScreen extends React.Component<Props, State> {
<Title>{i18n.t('aboutScreen.author')}</Title> <Title>{i18n.t('aboutScreen.author')}</Title>
<FlatList <FlatList
data={this.authorData} data={this.authorData}
extraData={this.state}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
listKey={"1"} listKey={"team1"}
renderItem={this.getCardItem} renderItem={this.getCardItem}
/> />
<Title>{i18n.t('aboutScreen.additionalDev')}</Title> <Title>{i18n.t('aboutScreen.additionalDev')}</Title>
<FlatList <FlatList
data={this.additionalDevData} data={this.additionalDevData}
extraData={this.state}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
listKey={"2"} listKey={"team2"}
renderItem={this.getCardItem} renderItem={this.getCardItem}
/> />
</Card.Content> </Card.Content>
@ -295,7 +299,9 @@ class AboutScreen extends React.Component<Props, State> {
<Title>{i18n.t('aboutScreen.technologies')}</Title> <Title>{i18n.t('aboutScreen.technologies')}</Title>
<FlatList <FlatList
data={this.technoData} data={this.technoData}
extraData={this.state}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
listKey={"techno"}
renderItem={this.getCardItem} renderItem={this.getCardItem}
/> />
</Card.Content> </Card.Content>
@ -398,24 +404,28 @@ class AboutScreen extends React.Component<Props, State> {
<Button <Button
icon="email" icon="email"
mode="contained" mode="contained"
dark={true}
color={this.colors.primary}
style={{ style={{
marginTop: 20, marginTop: 20,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}} }}
onPress={this.onPressMail}> onPress={this.onPressMail}>
{i18n.t('aboutScreen.bugsMail')} <Text>{i18n.t('aboutScreen.bugsMail')}</Text>
</Button> </Button>
<Button <Button
icon="git" icon="git"
mode="contained" mode="contained"
dark={true}
color={this.colors.primary}
style={{ style={{
marginTop: 20, marginTop: 20,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}} }}
onPress={this.onPressGit}> onPress={this.onPressGit}>
{i18n.t('aboutScreen.bugsGit')} <Text>{i18n.t('aboutScreen.bugsGit')}</Text>
</Button> </Button>
</View> </View>
); );
@ -466,6 +476,8 @@ class AboutScreen extends React.Component<Props, State> {
<FlatList <FlatList
style={{padding: 5}} style={{padding: 5}}
data={this.dataOrder} data={this.dataOrder}
extraData={this.state}
keyExtractor={(item) => item.id}
renderItem={this.getMainCard} renderItem={this.getMainCard}
/> />
</View> </View>

View file

@ -32,8 +32,8 @@ const emailRegex = /^.+@.+\..+$/;
class LoginScreen extends React.Component<Props, State> { class LoginScreen extends React.Component<Props, State> {
state = { state = {
email: 'vergnet@etud.insa-toulouse.fr', email: '',
password: '3D514ùdsqg', password: '',
isEmailValidated: false, isEmailValidated: false,
isPasswordValidated: false, isPasswordValidated: false,
loading: false, loading: false,

View file

@ -12,8 +12,6 @@ import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardI
import {stringToDate} from "../utils/Planning"; import {stringToDate} from "../utils/Planning";
import {openBrowser} from "../utils/WebBrowser"; import {openBrowser} from "../utils/WebBrowser";
import ActionsDashBoardItem from "../components/Home/ActionsDashboardItem"; import ActionsDashBoardItem from "../components/Home/ActionsDashboardItem";
import HeaderButton from "../components/Custom/HeaderButton";
import ConnectionManager from "../managers/ConnectionManager";
// import DATA from "../dashboard_data.json"; // import DATA from "../dashboard_data.json";
@ -32,10 +30,15 @@ type Props = {
theme: Object, theme: Object,
} }
type State = {
imageModalVisible: boolean,
imageList: Array<Object>,
}
/** /**
* Class defining the app's home screen * Class defining the app's home screen
*/ */
class HomeScreen extends React.Component<Props> { class HomeScreen extends React.Component<Props, State> {
onProxiwashClick: Function; onProxiwashClick: Function;
onTutorInsaClick: Function; onTutorInsaClick: Function;
@ -46,7 +49,10 @@ class HomeScreen extends React.Component<Props> {
colors: Object; colors: Object;
isLoggedIn: boolean | null; state = {
imageModalVisible: false,
imageList: [],
};
constructor(props) { constructor(props) {
super(props); super(props);
@ -57,8 +63,6 @@ class HomeScreen extends React.Component<Props> {
this.getRenderItem = this.getRenderItem.bind(this); this.getRenderItem = this.getRenderItem.bind(this);
this.createDataset = this.createDataset.bind(this); this.createDataset = this.createDataset.bind(this);
this.colors = props.theme.colors; this.colors = props.theme.colors;
this.isLoggedIn = null;
} }
/** /**
@ -72,35 +76,6 @@ class HomeScreen extends React.Component<Props> {
return date.toLocaleString(); return date.toLocaleString();
} }
componentDidMount() {
this.props.navigation.addListener('focus', this.onScreenFocus);
}
onScreenFocus = () => {
if (this.isLoggedIn !== ConnectionManager.getInstance().isLoggedIn()) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
this.props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
};
getHeaderButton = () => {
const screen = this.isLoggedIn
? "ProfileScreen"
: "LoginScreen";
const icon = this.isLoggedIn
? "account"
: "login";
const onPress = () => this.props.navigation.navigate(screen);
return <HeaderButton
icon={icon}
onPress={onPress}
color={this.isLoggedIn ? undefined : this.colors.primary}
/>;
};
onProxiwashClick() { onProxiwashClick() {
this.props.navigation.navigate('Proxiwash'); this.props.navigation.navigate('Proxiwash');
} }
@ -117,6 +92,16 @@ class HomeScreen extends React.Component<Props> {
this.props.navigation.navigate('SelfMenuScreen'); this.props.navigation.navigate('SelfMenuScreen');
} }
/**
* Extract a key for the given item
*
* @param item The item to extract the key from
* @return {*} The extracted key
*/
getKeyExtractor(item: Object) {
return item !== undefined ? item.id : undefined;
}
/** /**
* Creates the dataset to be used in the FlatList * Creates the dataset to be used in the FlatList
* *
@ -135,11 +120,15 @@ class HomeScreen extends React.Component<Props> {
{ {
title: '', title: '',
data: dashboardData, data: dashboardData,
extraData: super.state,
keyExtractor: this.getKeyExtractor,
id: SECTIONS_ID[0] id: SECTIONS_ID[0]
}, },
{ {
title: i18n.t('homeScreen.newsFeed'), title: i18n.t('homeScreen.newsFeed'),
data: newsData, data: newsData,
extraData: super.state,
keyExtractor: this.getKeyExtractor,
id: SECTIONS_ID[1] id: SECTIONS_ID[1]
} }
]; ];
@ -437,6 +426,18 @@ class HomeScreen extends React.Component<Props> {
openBrowser(link, this.colors.primary); openBrowser(link, this.colors.primary);
} }
showImageModal(imageList) {
this.setState({
imageModalVisible: true,
imageList: imageList,
});
};
hideImageModal = () => {
this.setState({imageModalVisible: false});
};
/** /**
* Gets a render item for the given feed object * Gets a render item for the given feed object
* *
@ -471,6 +472,7 @@ class HomeScreen extends React.Component<Props> {
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;
return ( return (
<View>
<WebSectionList <WebSectionList
createDataset={this.createDataset} createDataset={this.createDataset}
navigation={nav} navigation={nav}
@ -478,6 +480,8 @@ class HomeScreen extends React.Component<Props> {
refreshOnFocus={true} refreshOnFocus={true}
fetchUrl={DATA_URL} fetchUrl={DATA_URL}
renderItem={this.getRenderItem}/> renderItem={this.getRenderItem}/>
</View>
); );
} }
} }

View file

@ -1,12 +1,11 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, Image, Platform, ScrollView, View} from "react-native"; import {Image, Platform, ScrollView, View} from "react-native";
import i18n from "i18n-js"; import i18n from "i18n-js";
import CustomModal from "../../components/Custom/CustomModal"; import CustomModal from "../../components/Custom/CustomModal";
import {IconButton, RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper"; import {Avatar, IconButton, List, RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper";
import {stringMatchQuery} from "../../utils/Search"; import PureFlatList from "../../components/Lists/PureFlatList";
import ProximoListItem from "../../components/Lists/ProximoListItem";
function sortPrice(a, b) { function sortPrice(a, b) {
return a.price - b.price; return a.price - b.price;
@ -40,7 +39,7 @@ type Props = {
type State = { type State = {
currentSortMode: number, currentSortMode: number,
modalCurrentDisplayItem: React.Node, modalCurrentDisplayItem: React.Node,
currentSearchString: string, currentlyDisplayedData: Array<Object>,
}; };
/** /**
@ -49,21 +48,30 @@ type State = {
class ProximoListScreen extends React.Component<Props, State> { class ProximoListScreen extends React.Component<Props, State> {
modalRef: Object; modalRef: Object;
listData: Array<Object>; originalData: Array<Object>;
shouldFocusSearchBar: boolean; shouldFocusSearchBar: boolean;
onSearchStringChange: Function;
onSortMenuPress: Function;
renderItem: Function;
onModalRef: Function;
colors: Object; colors: Object;
constructor(props) { constructor(props) {
super(props); super(props);
this.listData = this.props.route.params['data']['data']; this.originalData = this.props.route.params['data']['data'];
this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar']; this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar'];
this.state = { this.state = {
currentSearchString: '', currentlyDisplayedData: this.originalData.sort(sortName),
currentSortMode: 3, currentSortMode: 3,
modalCurrentDisplayItem: null, modalCurrentDisplayItem: null,
}; };
this.onSearchStringChange = this.onSearchStringChange.bind(this);
this.onSortMenuPress = this.onSortMenuPress.bind(this);
this.renderItem = this.renderItem.bind(this);
this.onModalRef = this.onModalRef.bind(this);
this.colors = props.theme.colors; this.colors = props.theme.colors;
} }
@ -72,9 +80,11 @@ class ProximoListScreen extends React.Component<Props, State> {
* Creates the header content * Creates the header content
*/ */
componentDidMount() { componentDidMount() {
const button = this.getSortMenuButton.bind(this);
const title = this.getSearchBar.bind(this);
this.props.navigation.setOptions({ this.props.navigation.setOptions({
headerRight: this.getSortMenuButton, headerRight: button,
headerTitle: this.getSearchBar, headerTitle: title,
headerBackTitleVisible: false, headerBackTitleVisible: false,
headerTitleContainerStyle: Platform.OS === 'ios' ? headerTitleContainerStyle: Platform.OS === 'ios' ?
{marginHorizontal: 0, width: '70%'} : {marginHorizontal: 0, width: '70%'} :
@ -87,21 +97,21 @@ class ProximoListScreen extends React.Component<Props, State> {
* *
* @return {*} * @return {*}
*/ */
getSearchBar = () => { getSearchBar() {
return ( return (
<Searchbar <Searchbar
placeholder={i18n.t('proximoScreen.search')} placeholder={i18n.t('proximoScreen.search')}
onChangeText={this.onSearchStringChange} onChangeText={this.onSearchStringChange}
/> />
); );
}; }
/** /**
* Gets the sort menu header button * Gets the sort menu header button
* *
* @return {*} * @return {*}
*/ */
getSortMenuButton = () => { getSortMenuButton() {
return ( return (
<IconButton <IconButton
icon="sort" icon="sort"
@ -110,20 +120,20 @@ class ProximoListScreen extends React.Component<Props, State> {
onPress={this.onSortMenuPress} onPress={this.onSortMenuPress}
/> />
); );
}; }
/** /**
* Callback used when clicking on the sort menu button. * Callback used when clicking on the sort menu button.
* It will open the modal to show a sort selection * It will open the modal to show a sort selection
*/ */
onSortMenuPress = () => { onSortMenuPress() {
this.setState({ this.setState({
modalCurrentDisplayItem: this.getModalSortMenu() modalCurrentDisplayItem: this.getModalSortMenu()
}); });
if (this.modalRef) { if (this.modalRef) {
this.modalRef.open(); this.modalRef.open();
} }
}; }
/** /**
* Sets the current sort mode. * Sets the current sort mode.
@ -134,18 +144,19 @@ class ProximoListScreen extends React.Component<Props, State> {
this.setState({ this.setState({
currentSortMode: mode, currentSortMode: mode,
}); });
let data = this.state.currentlyDisplayedData;
switch (mode) { switch (mode) {
case 1: case 1:
this.listData.sort(sortPrice); data.sort(sortPrice);
break; break;
case 2: case 2:
this.listData.sort(sortPriceReverse); data.sort(sortPriceReverse);
break; break;
case 3: case 3:
this.listData.sort(sortName); data.sort(sortName);
break; break;
case 4: case 4:
this.listData.sort(sortNameReverse); data.sort(sortNameReverse);
break; break;
} }
if (this.modalRef && mode !== this.state.currentSortMode) { if (this.modalRef && mode !== this.state.currentSortMode) {
@ -170,14 +181,46 @@ class ProximoListScreen extends React.Component<Props, State> {
return color; return color;
} }
/**
* Sanitizes the given string to improve search performance
*
* @param str The string to sanitize
* @return {string} The sanitized string
*/
sanitizeString(str: string): string {
return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
/**
* Returns only articles whose name contains the given string.
* Case and accents insensitive.
*
* @param str The string used to filter article names
* @returns {[]}
*/
filterData(str: string) {
let filteredData = [];
const testStr = this.sanitizeString(str);
const articles = this.originalData;
for (const article of articles) {
const name = this.sanitizeString(article.name);
if (name.includes(testStr)) {
filteredData.push(article)
}
}
return filteredData;
}
/** /**
* Callback used when the search changes * Callback used when the search changes
* *
* @param str The new search string * @param str The new search string
*/ */
onSearchStringChange = (str: string) => { onSearchStringChange(str: string) {
this.setState({currentSearchString: str}) this.setState({
}; currentlyDisplayedData: this.filterData(str)
})
}
/** /**
* Gets the modal content depending on the given article * Gets the modal content depending on the given article
@ -290,20 +333,23 @@ class ProximoListScreen extends React.Component<Props, State> {
* @param item The article to render * @param item The article to render
* @return {*} * @return {*}
*/ */
renderItem = ({item}: Object) => { renderItem({item}: Object) {
if (stringMatchQuery(item.name, this.state.currentSearchString)) {
const onPress = this.onListItemPress.bind(this, item); const onPress = this.onListItemPress.bind(this, item);
const color = this.getStockColor(parseInt(item.quantity));
return ( return (
<ProximoListItem <List.Item
item={item} title={item.name}
description={item.quantity + ' ' + i18n.t('proximoScreen.inStock')}
descriptionStyle={{color: this.getStockColor(parseInt(item.quantity))}}
onPress={onPress} onPress={onPress}
color={color} left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64}
source={{uri: item.image}}/>}
right={() =>
<Text style={{fontWeight: "bold"}}>
{item.price}
</Text>}
/> />
); );
} else }
return null;
};
/** /**
* Extracts a key for the given article * Extracts a key for the given article
@ -320,9 +366,9 @@ class ProximoListScreen extends React.Component<Props, State> {
* *
* @param ref * @param ref
*/ */
onModalRef = (ref: Object) => { onModalRef(ref: Object) {
this.modalRef = ref; this.modalRef = ref;
}; }
render() { render() {
return ( return (
@ -332,12 +378,11 @@ class ProximoListScreen extends React.Component<Props, State> {
<CustomModal onRef={this.onModalRef}> <CustomModal onRef={this.onModalRef}>
{this.state.modalCurrentDisplayItem} {this.state.modalCurrentDisplayItem}
</CustomModal> </CustomModal>
{/*$FlowFixMe*/} <PureFlatList
<FlatList data={this.state.currentlyDisplayedData}
data={this.listData}
extraData={this.state.currentSearchString + this.state.currentSortMode}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
renderItem={this.renderItem} renderItem={this.renderItem}
updateData={this.state.currentSortMode}
/> />
</View> </View>
); );

View file

@ -142,6 +142,7 @@ class ProximoMainScreen extends React.Component<Props, State> {
{ {
title: '', title: '',
data: this.generateData(fetchedData), data: this.generateData(fetchedData),
extraData: this.state,
keyExtractor: this.getKeyExtractor keyExtractor: this.getKeyExtractor
} }
]; ];

View file

@ -272,12 +272,14 @@ class ProxiwashScreen extends React.Component<Props, State> {
title: i18n.t('proxiwashScreen.dryers'), title: i18n.t('proxiwashScreen.dryers'),
icon: 'tumble-dryer', icon: 'tumble-dryer',
data: data.dryers === undefined ? [] : data.dryers, data: data.dryers === undefined ? [] : data.dryers,
extraData: this.state,
keyExtractor: this.getKeyExtractor keyExtractor: this.getKeyExtractor
}, },
{ {
title: i18n.t('proxiwashScreen.washers'), title: i18n.t('proxiwashScreen.washers'),
icon: 'washing-machine', icon: 'washing-machine',
data: data.washers === undefined ? [] : data.washers, data: data.washers === undefined ? [] : data.washers,
extraData: this.state,
keyExtractor: this.getKeyExtractor keyExtractor: this.getKeyExtractor
}, },
]; ];

View file

@ -56,6 +56,7 @@ class SelfMenuScreen extends React.Component<Props> {
{ {
title: '', title: '',
data: [], data: [],
extraData: super.state,
keyExtractor: this.getKeyExtractor keyExtractor: this.getKeyExtractor
} }
]; ];
@ -68,6 +69,7 @@ class SelfMenuScreen extends React.Component<Props> {
{ {
title: DateManager.getInstance().getTranslatedDate(fetchedData[i].date), title: DateManager.getInstance().getTranslatedDate(fetchedData[i].date),
data: fetchedData[i].meal[0].foodcategory, data: fetchedData[i].meal[0].foodcategory,
extraData: super.state,
keyExtractor: this.getKeyExtractor, keyExtractor: this.getKeyExtractor,
} }
); );

View file

@ -5,7 +5,7 @@ import ScoreManager from "./ScoreManager";
import type {coordinates} from './Shapes/BaseShape'; import type {coordinates} from './Shapes/BaseShape';
export type cell = {color: string, isEmpty: boolean, key: string}; export type cell = {color: string, isEmpty: boolean};
export type grid = Array<Array<cell>>; export type grid = Array<Array<cell>>;
/** /**
@ -50,7 +50,6 @@ export default class GridManager {
line.push({ line.push({
color: this.#colors.tetrisBackground, color: this.#colors.tetrisBackground,
isEmpty: true, isEmpty: true,
key: col.toString(),
}); });
} }
return line; return line;

View file

@ -57,7 +57,6 @@ export default class Piece {
grid[coord[i].y][coord[i].x] = { grid[coord[i].y][coord[i].x] = {
color: this.#colors.tetrisBackground, color: this.#colors.tetrisBackground,
isEmpty: true, isEmpty: true,
key: grid[coord[i].y][coord[i].x].key
}; };
} }
} }
@ -74,7 +73,6 @@ export default class Piece {
grid[coord[i].y][coord[i].x] = { grid[coord[i].y][coord[i].x] = {
color: this.#currentShape.getColor(), color: this.#currentShape.getColor(),
isEmpty: false, isEmpty: false,
key: grid[coord[i].y][coord[i].x].key
}; };
} }
} }

View file

@ -5,7 +5,9 @@ import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
type Props = { type Props = {
item: Object color: string,
isEmpty: boolean,
id: string,
} }
class Cell extends React.PureComponent<Props> { class Cell extends React.PureComponent<Props> {
@ -18,19 +20,17 @@ class Cell extends React.PureComponent<Props> {
} }
render() { render() {
const item = this.props.item;
return ( return (
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: item.isEmpty ? 'transparent' : item.color, backgroundColor: this.props.isEmpty ? 'transparent' : this.props.color,
borderColor: item.isEmpty ? 'transparent' : this.colors.tetrisBorder, borderColor: this.props.isEmpty ? 'transparent' : this.colors.tetrisBorder,
borderStyle: 'solid', borderStyle: 'solid',
borderRadius: 2, borderRadius: 2,
borderWidth: 1, borderWidth: 1,
aspectRatio: 1, aspectRatio: 1,
}} }}
key={item.key}
/> />
); );
} }

View file

@ -25,24 +25,22 @@ class Grid extends React.Component<Props> {
} }
getRow(rowNumber: number) { getRow(rowNumber: number) {
let cells = this.props.grid[rowNumber].map(this.getCellRender); let cells = [];
for (let i = 0; i < this.props.width; i++) {
let cell = this.props.grid[rowNumber][i];
let key = rowNumber + ':' + i;
cells.push(<Cell color={cell.color} isEmpty={cell.isEmpty} id={key}/>);
}
return( return(
<View <View style={{
style={{
flexDirection: 'row', flexDirection: 'row',
backgroundColor: this.props.backgroundColor, backgroundColor: this.props.backgroundColor,
}} }}>
key={rowNumber.toString()}
>
{cells} {cells}
</View> </View>
); );
} }
getCellRender = (item: Object) => {
return <Cell item={item} key={item.key}/>;
};
getGrid() { getGrid() {
let rows = []; let rows = [];
for (let i = 0; i < this.props.height; i++) { for (let i = 0; i < this.props.height; i++) {

View file

@ -22,24 +22,19 @@ class Preview extends React.PureComponent<Props> {
let grids = []; let grids = [];
for (let i = 0; i < this.props.next.length; i++) { for (let i = 0; i < this.props.next.length; i++) {
grids.push( grids.push(
this.getGridRender(this.props.next[i], i) <Grid
width={this.props.next[i][0].length}
height={this.props.next[i].length}
grid={this.props.next[i]}
containerMaxHeight={50}
containerMaxWidth={50}
backgroundColor={'transparent'}
/>
); );
} }
return grids; return grids;
} }
getGridRender(item: Object, index: number) {
return <Grid
width={item[0].length}
height={item.length}
grid={item}
containerMaxHeight={50}
containerMaxWidth={50}
backgroundColor={'transparent'}
key={index.toString()}
/>;
};
render() { render() {
if (this.props.next.length > 0) { if (this.props.next.length > 0) {
return ( return (

View file

@ -80,7 +80,6 @@
"homeScreen": { "homeScreen": {
"listUpdated": "List updated!", "listUpdated": "List updated!",
"listUpdateFail": "Error while updating list", "listUpdateFail": "Error while updating list",
"servicesButton": "More services",
"newsFeed": "Campus News", "newsFeed": "Campus News",
"dashboard": { "dashboard": {
"seeMore": "Click to see more", "seeMore": "Click to see more",

View file

@ -80,7 +80,6 @@
"homeScreen": { "homeScreen": {
"listUpdated": "List mise à jour!", "listUpdated": "List mise à jour!",
"listUpdateFail": "Erreur lors de la mise à jour de la liste", "listUpdateFail": "Erreur lors de la mise à jour de la liste",
"servicesButton": "Plus de services",
"newsFeed": "Nouvelles du campus", "newsFeed": "Nouvelles du campus",
"dashboard": { "dashboard": {
"seeMore": "Cliquez pour plus d'infos", "seeMore": "Cliquez pour plus d'infos",
@ -187,6 +186,7 @@
"error": "Il y a eu une erreur et il est impossible de récupérer les informations de cette machine. Veuillez nous excuser pour le gène occasionnée.", "error": "Il y a eu une erreur et il est impossible de récupérer les informations de cette machine. Veuillez nous excuser pour le gène occasionnée.",
"notificationErrorTitle": "Erreur", "notificationErrorTitle": "Erreur",
"notificationErrorDescription": "Impossible de créer les notifications. Merci de vérifier que vous avez activé les notifications puis redémarrez l'appli." "notificationErrorDescription": "Impossible de créer les notifications. Merci de vérifier que vous avez activé les notifications puis redémarrez l'appli."
}, },
"states": { "states": {
"finished": "TERMINÉ", "finished": "TERMINÉ",