From 71f39a64cc103d948131c3d36bf2891edb115ed8 Mon Sep 17 00:00:00 2001 From: Arnaud Vergnet Date: Wed, 8 Apr 2020 20:11:39 +0200 Subject: [PATCH] Improved app links and error handling on qr code opening --- src/components/Amicale/AuthenticatedScreen.js | 125 +++++++++++++----- src/components/Custom/ErrorView.js | 40 ++++-- src/components/Lists/WebSectionList.js | 1 + src/navigation/MainTabNavigator.js | 2 +- src/screens/About/DebugScreen.js | 1 - .../Amicale/Clubs/ClubDisplayScreen.js | 10 +- src/screens/Proxiwash/ProxiwashScreen.js | 2 +- src/screens/ScannerScreen.js | 1 - src/utils/URLHandler.js | 26 ++-- translations/en.json | 3 +- translations/fr.json | 3 +- 11 files changed, 147 insertions(+), 67 deletions(-) diff --git a/src/components/Amicale/AuthenticatedScreen.js b/src/components/Amicale/AuthenticatedScreen.js index c081524..032b2b9 100644 --- a/src/components/Amicale/AuthenticatedScreen.js +++ b/src/components/Amicale/AuthenticatedScreen.js @@ -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 { currentUserToken: string | null; connectionManager: ConnectionManager; - errorCode: number; - data: Array; - colors: Object; + errors: Array; + fetchedData: Array; - 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 { 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 { 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 { 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 ( ); } + /** + * Reloads the data, to be called using ref by parent components + */ reload() { this.fetchData(); } @@ -119,10 +172,10 @@ class AuthenticatedScreen extends React.Component { this.state.loading ? : (this.allRequestsValid() - ? this.props.renderFunction(this.data) + ? this.props.renderFunction(this.fetchedData) : this.getErrorRender()) ); } } -export default withTheme(AuthenticatedScreen); +export default AuthenticatedScreen; diff --git a/src/components/Custom/ErrorView.js b/src/components/Custom/ErrorView.js index 273dfed..4d302c2 100644 --- a/src/components/Custom/ErrorView.js +++ b/src/components/Custom/ErrorView.js @@ -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 { message: string; icon: string; + showLoginButton: boolean; + state = { refreshing: false, }; @@ -33,6 +36,7 @@ class ErrorView extends React.PureComponent { } 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 { 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 { } } + getRetryButton() { + return ; + } + + goToLogin = () => this.props.navigation.navigate("login"); + + getLoginButton() { + return ; + } + render() { this.generateMessage(); return ( @@ -89,14 +118,9 @@ class ErrorView extends React.PureComponent { }}> {this.message} - + {this.showLoginButton + ? this.getLoginButton() + : this.getRetryButton()} ); diff --git a/src/components/Lists/WebSectionList.js b/src/components/Lists/WebSectionList.js index 2a61379..06f4dee 100644 --- a/src/components/Lists/WebSectionList.js +++ b/src/components/Lists/WebSectionList.js @@ -194,6 +194,7 @@ export default class WebSectionList extends React.PureComponent { ListEmptyComponent={this.state.refreshing ? : } diff --git a/src/navigation/MainTabNavigator.js b/src/navigation/MainTabNavigator.js index 51a2735..c96d725 100644 --- a/src/navigation/MainTabNavigator.js +++ b/src/navigation/MainTabNavigator.js @@ -181,7 +181,7 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) { component={ClubDisplayScreen} options={({navigation}) => { return { - title: '', + title: i18n.t('screens.clubDisplayScreen'), ...TransitionPresets.ModalSlideFromBottomIOS, }; }} diff --git a/src/screens/About/DebugScreen.js b/src/screens/About/DebugScreen.js index e9e38ba..931eb8d 100644 --- a/src/screens/About/DebugScreen.js +++ b/src/screens/About/DebugScreen.js @@ -33,7 +33,6 @@ class DebugScreen extends React.Component { 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); diff --git a/src/screens/Amicale/Clubs/ClubDisplayScreen.js b/src/screens/Amicale/Clubs/ClubDisplayScreen.js index 8ff7597..56dea4e 100644 --- a/src/screens/Amicale/Clubs/ClubDisplayScreen.js +++ b/src/screens/Amicale/Clubs/ClubDisplayScreen.js @@ -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 { 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 { 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 { } 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 { {...this.props} links={[ { - link: 'clubs/list/' + this.clubId, - mandatory: false, + link: 'clubs/' + this.clubId, + mandatory: true, } ]} renderFunction={this.getScreen} diff --git a/src/screens/Proxiwash/ProxiwashScreen.js b/src/screens/Proxiwash/ProxiwashScreen.js index 0331a17..7521adf 100644 --- a/src/screens/Proxiwash/ProxiwashScreen.js +++ b/src/screens/Proxiwash/ProxiwashScreen.js @@ -235,7 +235,7 @@ class ProxiwashScreen extends React.Component { } /** - * Sets the given data as the watchlist + * Sets the given fetchedData as the watchlist * * @param data */ diff --git a/src/screens/ScannerScreen.js b/src/screens/ScannerScreen.js index 9b4c365..105f655 100644 --- a/src/screens/ScannerScreen.js +++ b/src/screens/ScannerScreen.js @@ -42,7 +42,6 @@ class ScannerScreen extends React.Component { updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"}); handleCodeScanned = ({type, data}) => { - if (!URLHandler.isUrlValid(data)) this.showErrorDialog(); else { diff --git a/src/utils/URLHandler.js b/src/utils/URLHandler.js index b907057..d7c6c5c 100644 --- a/src/utils/URLHandler.js +++ b/src/utils/URLHandler.js @@ -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) { - 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) { - 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}}; } diff --git a/translations/en.json b/translations/en.json index 2c0f87e..ef2ff6f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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.", diff --git a/translations/fr.json b/translations/fr.json index b66cb56..a751018 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -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.",