forked from vergnet/application-amicale
Improved app links and error handling on qr code opening
This commit is contained in:
parent
53daa6671a
commit
71f39a64cc
11 changed files with 147 additions and 67 deletions
|
@ -1,14 +1,12 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
|
||||
import ErrorView from "../Custom/ErrorView";
|
||||
import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
|
||||
|
||||
type Props = {
|
||||
navigation: Object,
|
||||
theme: Object,
|
||||
links: Array<{link: string, mandatory: boolean}>,
|
||||
renderFunction: Function,
|
||||
}
|
||||
|
@ -25,24 +23,35 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
|||
|
||||
currentUserToken: string | null;
|
||||
connectionManager: ConnectionManager;
|
||||
errorCode: number;
|
||||
data: Array<Object>;
|
||||
colors: Object;
|
||||
errors: Array<number>;
|
||||
fetchedData: Array<Object>;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.colors = props.theme.colors;
|
||||
this.connectionManager = ConnectionManager.getInstance();
|
||||
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
|
||||
this.data = new Array(this.props.links.length);
|
||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.fetchedData = new Array(this.props.links.length);
|
||||
this.errors = new Array(this.props.links.length);
|
||||
this.fetchData(); // TODO remove in prod (only use for fast refresh)
|
||||
}
|
||||
|
||||
onScreenFocus() {
|
||||
if (this.currentUserToken !== this.connectionManager.getToken())
|
||||
/**
|
||||
* Refreshes screen if user changed
|
||||
*/
|
||||
onScreenFocus = () => {
|
||||
if (this.currentUserToken !== this.connectionManager.getToken()){
|
||||
this.currentUserToken = this.connectionManager.getToken();
|
||||
this.fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the data from the server.
|
||||
*
|
||||
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
|
||||
*
|
||||
* If the user is logged in, send all requests.
|
||||
*/
|
||||
fetchData = () => {
|
||||
if (!this.state.loading)
|
||||
this.setState({loading: true});
|
||||
|
@ -50,39 +59,51 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
|||
for (let i = 0; i < this.props.links.length; i++) {
|
||||
this.connectionManager.authenticatedRequest(this.props.links[i].link, null, null)
|
||||
.then((data) => {
|
||||
this.onFinishedLoading(data, i, -1);
|
||||
this.onRequestFinished(data, i, -1);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.onFinishedLoading(null, i, error);
|
||||
this.onRequestFinished(null, i, error);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
this.onFinishedLoading(null, -1, ERROR_TYPE.BAD_CREDENTIALS);
|
||||
for (let i = 0; i < this.props.links.length; i++) {
|
||||
this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onFinishedLoading(data: Object, index: number, error: number) {
|
||||
if (index >= 0 && index < this.props.links.length)
|
||||
this.data[index] = data;
|
||||
this.currentUserToken = data !== undefined
|
||||
? this.connectionManager.getToken()
|
||||
: null;
|
||||
this.errorCode = error;
|
||||
/**
|
||||
* Callback used when a request finishes, successfully or not.
|
||||
* Saves data and error code.
|
||||
* If the token is invalid, logout the user and open the login screen.
|
||||
* If the last request was received, stop the loading screen.
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @param index The index for the data
|
||||
* @param error The error code received
|
||||
*/
|
||||
onRequestFinished(data: Object | null, index: number, error: number) {
|
||||
if (index >= 0 && index < this.props.links.length){
|
||||
this.fetchedData[index] = data;
|
||||
this.errors[index] = error;
|
||||
}
|
||||
|
||||
if (this.errorCode === ERROR_TYPE.BAD_TOKEN) { // Token expired, logout user
|
||||
this.connectionManager.disconnect()
|
||||
.then(() => {
|
||||
this.props.navigation.navigate("login");
|
||||
});
|
||||
} else if (this.allRequestsFinished())
|
||||
if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user
|
||||
this.connectionManager.disconnect();
|
||||
|
||||
if (this.allRequestsFinished())
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all requests finished processing
|
||||
*
|
||||
* @return {boolean} True if all finished
|
||||
*/
|
||||
allRequestsFinished() {
|
||||
let finished = true;
|
||||
for (let i = 0; i < this.data.length; i++) {
|
||||
if (this.data[i] === undefined) {
|
||||
for (let i = 0; i < this.fetchedData.length; i++) {
|
||||
if (this.fetchedData[i] === undefined) {
|
||||
finished = false;
|
||||
break;
|
||||
}
|
||||
|
@ -90,10 +111,17 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
|||
return finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all requests have finished successfully.
|
||||
* This will return false only if a mandatory request failed.
|
||||
* All non-mandatory requests can fail without impacting the return value.
|
||||
*
|
||||
* @return {boolean} True if all finished successfully
|
||||
*/
|
||||
allRequestsValid() {
|
||||
let valid = true;
|
||||
for (let i = 0; i < this.data.length; i++) {
|
||||
if (this.data[i] === null && this.props.links[i].mandatory) {
|
||||
for (let i = 0; i < this.fetchedData.length; i++) {
|
||||
if (this.fetchedData[i] === null && this.props.links[i].mandatory) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
@ -101,15 +129,40 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
|||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error to render.
|
||||
* Non-mandatory requests are ignored.
|
||||
*
|
||||
*
|
||||
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
|
||||
*/
|
||||
getError() {
|
||||
for (let i = 0; i < this.errors.length; i++) {
|
||||
if (this.errors[i] !== 0 && this.props.links[i].mandatory) {
|
||||
return this.errors[i];
|
||||
}
|
||||
}
|
||||
return ERROR_TYPE.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error view to display in case of error
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getErrorRender() {
|
||||
return (
|
||||
<ErrorView
|
||||
errorCode={this.errorCode}
|
||||
{...this.props}
|
||||
errorCode={this.getError()}
|
||||
onRefresh={this.fetchData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the data, to be called using ref by parent components
|
||||
*/
|
||||
reload() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
@ -119,10 +172,10 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
|||
this.state.loading
|
||||
? <BasicLoadingScreen/>
|
||||
: (this.allRequestsValid()
|
||||
? this.props.renderFunction(this.data)
|
||||
? this.props.renderFunction(this.fetchedData)
|
||||
: this.getErrorRender())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AuthenticatedScreen);
|
||||
export default AuthenticatedScreen;
|
||||
|
|
|
@ -8,6 +8,7 @@ import i18n from 'i18n-js';
|
|||
import {ERROR_TYPE} from "../../managers/ConnectionManager";
|
||||
|
||||
type Props = {
|
||||
navigation: Object,
|
||||
errorCode: number,
|
||||
onRefresh: Function,
|
||||
}
|
||||
|
@ -23,6 +24,8 @@ class ErrorView extends React.PureComponent<Props, State> {
|
|||
message: string;
|
||||
icon: string;
|
||||
|
||||
showLoginButton: boolean;
|
||||
|
||||
state = {
|
||||
refreshing: false,
|
||||
};
|
||||
|
@ -33,6 +36,7 @@ class ErrorView extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
generateMessage() {
|
||||
this.showLoginButton = false;
|
||||
switch (this.props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t("errors.badCredentials");
|
||||
|
@ -41,6 +45,7 @@ class ErrorView extends React.PureComponent<Props, State> {
|
|||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t("errors.badToken");
|
||||
this.icon = "account-alert-outline";
|
||||
this.showLoginButton = true;
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t("errors.noConsent");
|
||||
|
@ -69,6 +74,30 @@ class ErrorView extends React.PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
getRetryButton() {
|
||||
return <Button
|
||||
mode={'contained'}
|
||||
icon={'refresh'}
|
||||
onPress={this.props.onRefresh}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t("general.retry")}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
goToLogin = () => this.props.navigation.navigate("login");
|
||||
|
||||
getLoginButton() {
|
||||
return <Button
|
||||
mode={'contained'}
|
||||
icon={'login'}
|
||||
onPress={this.goToLogin}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t("screens.login")}
|
||||
</Button>;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.generateMessage();
|
||||
return (
|
||||
|
@ -89,14 +118,9 @@ class ErrorView extends React.PureComponent<Props, State> {
|
|||
}}>
|
||||
{this.message}
|
||||
</Subheading>
|
||||
<Button
|
||||
mode={'contained'}
|
||||
icon={'refresh'}
|
||||
onPress={this.props.onRefresh}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t("general.retry")}
|
||||
</Button>
|
||||
{this.showLoginButton
|
||||
? this.getLoginButton()
|
||||
: this.getRetryButton()}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -194,6 +194,7 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
|
|||
ListEmptyComponent={this.state.refreshing
|
||||
? <BasicLoadingScreen/>
|
||||
: <ErrorView
|
||||
{...this.props}
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefresh}/>
|
||||
}
|
||||
|
|
|
@ -181,7 +181,7 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
|
|||
component={ClubDisplayScreen}
|
||||
options={({navigation}) => {
|
||||
return {
|
||||
title: '',
|
||||
title: i18n.t('screens.clubDisplayScreen'),
|
||||
...TransitionPresets.ModalSlideFromBottomIOS,
|
||||
};
|
||||
}}
|
||||
|
|
|
@ -33,7 +33,6 @@ class DebugScreen extends React.Component<Props, State> {
|
|||
this.onModalRef = this.onModalRef.bind(this);
|
||||
this.colors = props.theme.colors;
|
||||
let copy = {...AsyncStorageManager.getInstance().preferences};
|
||||
console.log(copy);
|
||||
let currentPreferences = [];
|
||||
Object.values(copy).map((object) => {
|
||||
currentPreferences.push(object);
|
||||
|
|
|
@ -39,7 +39,7 @@ const FakeClub = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Class defining a planning event information page.
|
||||
* Class defining a club event information page.
|
||||
* If called with data and categories navigation parameters, will use those to display the data.
|
||||
* If called with clubId parameter, will fetch the information on the server
|
||||
*/
|
||||
|
@ -61,7 +61,6 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
|||
super(props);
|
||||
this.colors = props.theme.colors;
|
||||
|
||||
console.log(this.props.route.params);
|
||||
if (this.props.route.params.data !== undefined && this.props.route.params.categories !== undefined) {
|
||||
this.displayData = this.props.route.params.data;
|
||||
this.categories = this.props.route.params.categories;
|
||||
|
@ -72,7 +71,6 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
|||
this.categories = null;
|
||||
this.clubId = this.props.route.params.clubId;
|
||||
this.shouldFetchData = true;
|
||||
console.log(this.clubId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,6 +133,8 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
getScreen = (data: Object) => {
|
||||
console.log('fetchedData passed to screen:');
|
||||
console.log(data);
|
||||
data = FakeClub;
|
||||
this.updateHeaderTitle(data);
|
||||
|
||||
|
@ -183,8 +183,8 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
|||
{...this.props}
|
||||
links={[
|
||||
{
|
||||
link: 'clubs/list/' + this.clubId,
|
||||
mandatory: false,
|
||||
link: 'clubs/' + this.clubId,
|
||||
mandatory: true,
|
||||
}
|
||||
]}
|
||||
renderFunction={this.getScreen}
|
||||
|
|
|
@ -235,7 +235,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the given data as the watchlist
|
||||
* Sets the given fetchedData as the watchlist
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
|
|
|
@ -42,7 +42,6 @@ class ScannerScreen extends React.Component<Props, State> {
|
|||
updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"});
|
||||
|
||||
handleCodeScanned = ({type, data}) => {
|
||||
|
||||
if (!URLHandler.isUrlValid(data))
|
||||
this.showErrorDialog();
|
||||
else {
|
||||
|
|
|
@ -4,6 +4,9 @@ import {Linking} from 'expo';
|
|||
|
||||
export default class URLHandler {
|
||||
|
||||
static CLUB_INFO_URL_PATH = "club";
|
||||
static EVENT_INFO_URL_PATH = "event";
|
||||
|
||||
static CLUB_INFO_ROUTE = "club-information";
|
||||
static EVENT_INFO_ROUTE = "planning-information";
|
||||
|
||||
|
@ -16,7 +19,6 @@ export default class URLHandler {
|
|||
}
|
||||
|
||||
listen() {
|
||||
console.log(Linking.makeUrl('main/home/club-information', {clubId: 1}));
|
||||
Linking.addEventListener('url', this.onUrl);
|
||||
Linking.parseInitialURLAsync().then(this.onInitialUrl);
|
||||
}
|
||||
|
@ -34,12 +36,12 @@ export default class URLHandler {
|
|||
};
|
||||
|
||||
static getUrlData({path, queryParams}: Object) {
|
||||
console.log(path);
|
||||
let data = null;
|
||||
if (path !== null) {
|
||||
let pathArray = path.split('/');
|
||||
if (URLHandler.isClubInformationLink(pathArray))
|
||||
if (URLHandler.isClubInformationLink(path))
|
||||
data = URLHandler.generateClubInformationData(queryParams);
|
||||
else if (URLHandler.isPlanningInformationLink(pathArray))
|
||||
else if (URLHandler.isPlanningInformationLink(path))
|
||||
data = URLHandler.generatePlanningInformationData(queryParams);
|
||||
}
|
||||
return data;
|
||||
|
@ -49,17 +51,17 @@ export default class URLHandler {
|
|||
return this.getUrlData(Linking.parse(url)) !== null;
|
||||
}
|
||||
|
||||
static isClubInformationLink(pathArray: Array<string>) {
|
||||
return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "club-information";
|
||||
static isClubInformationLink(path: string) {
|
||||
return path === URLHandler.CLUB_INFO_URL_PATH;
|
||||
}
|
||||
|
||||
static isPlanningInformationLink(pathArray: Array<string>) {
|
||||
return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "planning-information";
|
||||
static isPlanningInformationLink(path: string) {
|
||||
return path === URLHandler.EVENT_INFO_URL_PATH;
|
||||
}
|
||||
|
||||
static generateClubInformationData(params: Object): Object | null {
|
||||
if (params !== undefined && params.clubId !== undefined) {
|
||||
let id = parseInt(params.clubId);
|
||||
if (params !== undefined && params.id !== undefined) {
|
||||
let id = parseInt(params.id);
|
||||
if (!isNaN(id)) {
|
||||
return {route: URLHandler.CLUB_INFO_ROUTE, data: {clubId: id}};
|
||||
}
|
||||
|
@ -68,8 +70,8 @@ export default class URLHandler {
|
|||
}
|
||||
|
||||
static generatePlanningInformationData(params: Object): Object | null {
|
||||
if (params !== undefined && params.eventId !== undefined) {
|
||||
let id = parseInt(params.eventId);
|
||||
if (params !== undefined && params.id !== undefined) {
|
||||
let id = parseInt(params.id);
|
||||
if (!isNaN(id)) {
|
||||
return {route: URLHandler.EVENT_INFO_ROUTE, data: {eventId: id}};
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"home": "Home",
|
||||
"planning": "Planning",
|
||||
"planningDisplayScreen": "Event details",
|
||||
"clubDisplayScreen": "Club details",
|
||||
"proxiwash": "Proxiwash",
|
||||
"proximo": "Proximo",
|
||||
"proximoArticles": "Articles",
|
||||
|
@ -253,7 +254,7 @@
|
|||
"errors": {
|
||||
"title": "Error!",
|
||||
"badCredentials": "Email or password invalid.",
|
||||
"badToken": "Session expired, please login again.",
|
||||
"badToken": "You are not logged in. Please login and try again.",
|
||||
"noConsent": "You did not give your consent for data processing to the Amicale.",
|
||||
"badInput": "Invalid input. Please try again.",
|
||||
"forbidden": "You do not have access to this data.",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"home": "Accueil",
|
||||
"planning": "Planning",
|
||||
"planningDisplayScreen": "Détails",
|
||||
"clubDisplayScreen": "Détails",
|
||||
"proxiwash": "Proxiwash",
|
||||
"proximo": "Proximo",
|
||||
"proximoArticles": "Articles",
|
||||
|
@ -253,7 +254,7 @@
|
|||
"errors": {
|
||||
"title": "Erreur !",
|
||||
"badCredentials": "Email ou mot de passe invalide.",
|
||||
"badToken": "Session expirée, merci de vous reconnecter.",
|
||||
"badToken": "Vous n'êtes pas connecté. Merci de vous connecter puis réessayez.",
|
||||
"noConsent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
|
||||
"badInput": "Entrée invalide. Merci de réessayer.",
|
||||
"forbidden": "Vous n'avez pas accès à cette information.",
|
||||
|
|
Loading…
Reference in a new issue