Improved app links and error handling on qr code opening

This commit is contained in:
Arnaud Vergnet 2020-04-08 20:11:39 +02:00
parent 53daa6671a
commit 71f39a64cc
11 changed files with 147 additions and 67 deletions

View file

@ -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;

View file

@ -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>
);

View file

@ -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}/>
}

View file

@ -181,7 +181,7 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
component={ClubDisplayScreen}
options={({navigation}) => {
return {
title: '',
title: i18n.t('screens.clubDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS,
};
}}

View file

@ -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);

View file

@ -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}

View file

@ -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
*/

View file

@ -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 {

View file

@ -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}};
}

View file

@ -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.",

View file

@ -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.",