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