Browse Source

Improved app links and error handling on qr code opening

Arnaud Vergnet 4 years ago
parent
commit
71f39a64cc

+ 90
- 37
src/components/Amicale/AuthenticatedScreen.js View File

1
 // @flow
1
 // @flow
2
 
2
 
3
 import * as React from 'react';
3
 import * as React from 'react';
4
-import {withTheme} from 'react-native-paper';
5
 import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
4
 import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
6
 import ErrorView from "../Custom/ErrorView";
5
 import ErrorView from "../Custom/ErrorView";
7
 import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
6
 import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
8
 
7
 
9
 type Props = {
8
 type Props = {
10
     navigation: Object,
9
     navigation: Object,
11
-    theme: Object,
12
     links: Array<{link: string, mandatory: boolean}>,
10
     links: Array<{link: string, mandatory: boolean}>,
13
     renderFunction: Function,
11
     renderFunction: Function,
14
 }
12
 }
25
 
23
 
26
     currentUserToken: string | null;
24
     currentUserToken: string | null;
27
     connectionManager: ConnectionManager;
25
     connectionManager: ConnectionManager;
28
-    errorCode: number;
29
-    data: Array<Object>;
30
-    colors: Object;
26
+    errors: Array<number>;
27
+    fetchedData: Array<Object>;
31
 
28
 
32
-    constructor(props) {
29
+    constructor(props: Object) {
33
         super(props);
30
         super(props);
34
-        this.colors = props.theme.colors;
35
         this.connectionManager = ConnectionManager.getInstance();
31
         this.connectionManager = ConnectionManager.getInstance();
36
-        this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
37
-        this.data = new Array(this.props.links.length);
32
+        this.props.navigation.addListener('focus', this.onScreenFocus);
33
+        this.fetchedData = new Array(this.props.links.length);
34
+        this.errors = new Array(this.props.links.length);
38
         this.fetchData(); // TODO remove in prod (only use for fast refresh)
35
         this.fetchData(); // TODO remove in prod (only use for fast refresh)
39
     }
36
     }
40
 
37
 
41
-    onScreenFocus() {
42
-        if (this.currentUserToken !== this.connectionManager.getToken())
38
+    /**
39
+     * Refreshes screen if user changed
40
+     */
41
+    onScreenFocus = () => {
42
+        if (this.currentUserToken !== this.connectionManager.getToken()){
43
+            this.currentUserToken = this.connectionManager.getToken();
43
             this.fetchData();
44
             this.fetchData();
44
-    }
45
+        }
46
+    };
45
 
47
 
48
+    /**
49
+     * Fetches the data from the server.
50
+     *
51
+     * If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
52
+     *
53
+     * If the user is logged in, send all requests.
54
+     */
46
     fetchData = () => {
55
     fetchData = () => {
47
         if (!this.state.loading)
56
         if (!this.state.loading)
48
             this.setState({loading: true});
57
             this.setState({loading: true});
50
             for (let i = 0; i < this.props.links.length; i++) {
59
             for (let i = 0; i < this.props.links.length; i++) {
51
                 this.connectionManager.authenticatedRequest(this.props.links[i].link, null, null)
60
                 this.connectionManager.authenticatedRequest(this.props.links[i].link, null, null)
52
                     .then((data) => {
61
                     .then((data) => {
53
-                        this.onFinishedLoading(data, i, -1);
62
+                        this.onRequestFinished(data, i, -1);
54
                     })
63
                     })
55
                     .catch((error) => {
64
                     .catch((error) => {
56
-                        this.onFinishedLoading(null, i, error);
65
+                        this.onRequestFinished(null, i, error);
57
                     });
66
                     });
58
             }
67
             }
59
-
60
         } else {
68
         } else {
61
-            this.onFinishedLoading(null, -1, ERROR_TYPE.BAD_CREDENTIALS);
69
+            for (let i = 0; i < this.props.links.length; i++) {
70
+                this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
71
+            }
62
         }
72
         }
63
     };
73
     };
64
 
74
 
65
-    onFinishedLoading(data: Object, index: number, error: number) {
66
-        if (index >= 0 && index < this.props.links.length)
67
-            this.data[index] = data;
68
-        this.currentUserToken = data !== undefined
69
-            ? this.connectionManager.getToken()
70
-            : null;
71
-        this.errorCode = error;
72
-
73
-        if (this.errorCode === ERROR_TYPE.BAD_TOKEN) { // Token expired, logout user
74
-            this.connectionManager.disconnect()
75
-                .then(() => {
76
-                    this.props.navigation.navigate("login");
77
-                });
78
-        } else if (this.allRequestsFinished())
75
+    /**
76
+     * Callback used when a request finishes, successfully or not.
77
+     * Saves data and error code.
78
+     * If the token is invalid, logout the user and open the login screen.
79
+     * If the last request was received, stop the loading screen.
80
+     *
81
+     * @param data The data fetched from the server
82
+     * @param index The index for the data
83
+     * @param error The error code received
84
+     */
85
+    onRequestFinished(data: Object | null, index: number, error: number) {
86
+        if (index >= 0 && index < this.props.links.length){
87
+            this.fetchedData[index] = data;
88
+            this.errors[index] = error;
89
+        }
90
+
91
+        if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user
92
+            this.connectionManager.disconnect();
93
+
94
+        if (this.allRequestsFinished())
79
             this.setState({loading: false});
95
             this.setState({loading: false});
80
     }
96
     }
81
 
97
 
98
+    /**
99
+     * Checks if all requests finished processing
100
+     *
101
+     * @return {boolean} True if all finished
102
+     */
82
     allRequestsFinished() {
103
     allRequestsFinished() {
83
         let finished = true;
104
         let finished = true;
84
-        for (let i = 0; i < this.data.length; i++) {
85
-            if (this.data[i] === undefined) {
105
+        for (let i = 0; i < this.fetchedData.length; i++) {
106
+            if (this.fetchedData[i] === undefined) {
86
                 finished = false;
107
                 finished = false;
87
                 break;
108
                 break;
88
             }
109
             }
90
         return finished;
111
         return finished;
91
     }
112
     }
92
 
113
 
114
+    /**
115
+     * Checks if all requests have finished successfully.
116
+     * This will return false only if a mandatory request failed.
117
+     * All non-mandatory requests can fail without impacting the return value.
118
+     *
119
+     * @return {boolean} True if all finished successfully
120
+     */
93
     allRequestsValid() {
121
     allRequestsValid() {
94
         let valid = true;
122
         let valid = true;
95
-        for (let i = 0; i < this.data.length; i++) {
96
-            if (this.data[i] === null && this.props.links[i].mandatory) {
123
+        for (let i = 0; i < this.fetchedData.length; i++) {
124
+            if (this.fetchedData[i] === null && this.props.links[i].mandatory) {
97
                 valid = false;
125
                 valid = false;
98
                 break;
126
                 break;
99
             }
127
             }
101
         return valid;
129
         return valid;
102
     }
130
     }
103
 
131
 
132
+    /**
133
+     * Gets the error to render.
134
+     * Non-mandatory requests are ignored.
135
+     *
136
+     *
137
+     * @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
138
+     */
139
+    getError() {
140
+        for (let i = 0; i < this.errors.length; i++) {
141
+            if (this.errors[i] !== 0 && this.props.links[i].mandatory) {
142
+                return this.errors[i];
143
+            }
144
+        }
145
+        return ERROR_TYPE.SUCCESS;
146
+    }
147
+
148
+    /**
149
+     * Gets the error view to display in case of error
150
+     *
151
+     * @return {*}
152
+     */
104
     getErrorRender() {
153
     getErrorRender() {
105
         return (
154
         return (
106
             <ErrorView
155
             <ErrorView
107
-                errorCode={this.errorCode}
156
+                {...this.props}
157
+                errorCode={this.getError()}
108
                 onRefresh={this.fetchData}
158
                 onRefresh={this.fetchData}
109
             />
159
             />
110
         );
160
         );
111
     }
161
     }
112
 
162
 
163
+    /**
164
+     * Reloads the data, to be called using ref by parent components
165
+     */
113
     reload() {
166
     reload() {
114
         this.fetchData();
167
         this.fetchData();
115
     }
168
     }
119
             this.state.loading
172
             this.state.loading
120
                 ? <BasicLoadingScreen/>
173
                 ? <BasicLoadingScreen/>
121
                 : (this.allRequestsValid()
174
                 : (this.allRequestsValid()
122
-                ? this.props.renderFunction(this.data)
175
+                ? this.props.renderFunction(this.fetchedData)
123
                 : this.getErrorRender())
176
                 : this.getErrorRender())
124
         );
177
         );
125
     }
178
     }
126
 }
179
 }
127
 
180
 
128
-export default withTheme(AuthenticatedScreen);
181
+export default AuthenticatedScreen;

+ 32
- 8
src/components/Custom/ErrorView.js View File

8
 import {ERROR_TYPE} from "../../managers/ConnectionManager";
8
 import {ERROR_TYPE} from "../../managers/ConnectionManager";
9
 
9
 
10
 type Props = {
10
 type Props = {
11
+    navigation: Object,
11
     errorCode: number,
12
     errorCode: number,
12
     onRefresh: Function,
13
     onRefresh: Function,
13
 }
14
 }
23
     message: string;
24
     message: string;
24
     icon: string;
25
     icon: string;
25
 
26
 
27
+    showLoginButton: boolean;
28
+
26
     state = {
29
     state = {
27
         refreshing: false,
30
         refreshing: false,
28
     };
31
     };
33
     }
36
     }
34
 
37
 
35
     generateMessage() {
38
     generateMessage() {
39
+        this.showLoginButton = false;
36
         switch (this.props.errorCode) {
40
         switch (this.props.errorCode) {
37
             case ERROR_TYPE.BAD_CREDENTIALS:
41
             case ERROR_TYPE.BAD_CREDENTIALS:
38
                 this.message = i18n.t("errors.badCredentials");
42
                 this.message = i18n.t("errors.badCredentials");
41
             case ERROR_TYPE.BAD_TOKEN:
45
             case ERROR_TYPE.BAD_TOKEN:
42
                 this.message = i18n.t("errors.badToken");
46
                 this.message = i18n.t("errors.badToken");
43
                 this.icon = "account-alert-outline";
47
                 this.icon = "account-alert-outline";
48
+                this.showLoginButton = true;
44
                 break;
49
                 break;
45
             case ERROR_TYPE.NO_CONSENT:
50
             case ERROR_TYPE.NO_CONSENT:
46
                 this.message = i18n.t("errors.noConsent");
51
                 this.message = i18n.t("errors.noConsent");
69
         }
74
         }
70
     }
75
     }
71
 
76
 
77
+    getRetryButton() {
78
+        return <Button
79
+            mode={'contained'}
80
+            icon={'refresh'}
81
+            onPress={this.props.onRefresh}
82
+            style={styles.button}
83
+        >
84
+            {i18n.t("general.retry")}
85
+        </Button>;
86
+    }
87
+
88
+    goToLogin = () => this.props.navigation.navigate("login");
89
+
90
+    getLoginButton() {
91
+        return <Button
92
+            mode={'contained'}
93
+            icon={'login'}
94
+            onPress={this.goToLogin}
95
+            style={styles.button}
96
+        >
97
+            {i18n.t("screens.login")}
98
+        </Button>;
99
+    }
100
+
72
     render() {
101
     render() {
73
         this.generateMessage();
102
         this.generateMessage();
74
         return (
103
         return (
89
                     }}>
118
                     }}>
90
                         {this.message}
119
                         {this.message}
91
                     </Subheading>
120
                     </Subheading>
92
-                    <Button
93
-                        mode={'contained'}
94
-                        icon={'refresh'}
95
-                        onPress={this.props.onRefresh}
96
-                        style={styles.button}
97
-                    >
98
-                        {i18n.t("general.retry")}
99
-                    </Button>
121
+                    {this.showLoginButton
122
+                        ? this.getLoginButton()
123
+                        : this.getRetryButton()}
100
                 </View>
124
                 </View>
101
             </View>
125
             </View>
102
         );
126
         );

+ 1
- 0
src/components/Lists/WebSectionList.js View File

194
                     ListEmptyComponent={this.state.refreshing
194
                     ListEmptyComponent={this.state.refreshing
195
                         ? <BasicLoadingScreen/>
195
                         ? <BasicLoadingScreen/>
196
                         : <ErrorView
196
                         : <ErrorView
197
+                            {...this.props}
197
                             errorCode={ERROR_TYPE.CONNECTION_ERROR}
198
                             errorCode={ERROR_TYPE.CONNECTION_ERROR}
198
                             onRefresh={this.onRefresh}/>
199
                             onRefresh={this.onRefresh}/>
199
                     }
200
                     }

+ 1
- 1
src/navigation/MainTabNavigator.js View File

181
                 component={ClubDisplayScreen}
181
                 component={ClubDisplayScreen}
182
                 options={({navigation}) => {
182
                 options={({navigation}) => {
183
                     return {
183
                     return {
184
-                        title: '',
184
+                        title: i18n.t('screens.clubDisplayScreen'),
185
                         ...TransitionPresets.ModalSlideFromBottomIOS,
185
                         ...TransitionPresets.ModalSlideFromBottomIOS,
186
                     };
186
                     };
187
                 }}
187
                 }}

+ 0
- 1
src/screens/About/DebugScreen.js View File

33
         this.onModalRef = this.onModalRef.bind(this);
33
         this.onModalRef = this.onModalRef.bind(this);
34
         this.colors = props.theme.colors;
34
         this.colors = props.theme.colors;
35
         let copy = {...AsyncStorageManager.getInstance().preferences};
35
         let copy = {...AsyncStorageManager.getInstance().preferences};
36
-        console.log(copy);
37
         let currentPreferences = [];
36
         let currentPreferences = [];
38
         Object.values(copy).map((object) => {
37
         Object.values(copy).map((object) => {
39
             currentPreferences.push(object);
38
             currentPreferences.push(object);

+ 5
- 5
src/screens/Amicale/Clubs/ClubDisplayScreen.js View File

39
 };
39
 };
40
 
40
 
41
 /**
41
 /**
42
- * Class defining a planning event information page.
42
+ * Class defining a club event information page.
43
  * If called with data and categories navigation parameters, will use those to display the data.
43
  * If called with data and categories navigation parameters, will use those to display the data.
44
  * If called with clubId parameter, will fetch the information on the server
44
  * If called with clubId parameter, will fetch the information on the server
45
  */
45
  */
61
         super(props);
61
         super(props);
62
         this.colors = props.theme.colors;
62
         this.colors = props.theme.colors;
63
 
63
 
64
-        console.log(this.props.route.params);
65
         if (this.props.route.params.data !== undefined && this.props.route.params.categories !== undefined) {
64
         if (this.props.route.params.data !== undefined && this.props.route.params.categories !== undefined) {
66
             this.displayData = this.props.route.params.data;
65
             this.displayData = this.props.route.params.data;
67
             this.categories = this.props.route.params.categories;
66
             this.categories = this.props.route.params.categories;
72
             this.categories = null;
71
             this.categories = null;
73
             this.clubId = this.props.route.params.clubId;
72
             this.clubId = this.props.route.params.clubId;
74
             this.shouldFetchData = true;
73
             this.shouldFetchData = true;
75
-            console.log(this.clubId);
76
         }
74
         }
77
     }
75
     }
78
 
76
 
135
     }
133
     }
136
 
134
 
137
     getScreen = (data: Object) => {
135
     getScreen = (data: Object) => {
136
+        console.log('fetchedData passed to screen:');
137
+        console.log(data);
138
         data = FakeClub;
138
         data = FakeClub;
139
         this.updateHeaderTitle(data);
139
         this.updateHeaderTitle(data);
140
 
140
 
183
                 {...this.props}
183
                 {...this.props}
184
                 links={[
184
                 links={[
185
                     {
185
                     {
186
-                        link: 'clubs/list/' + this.clubId,
187
-                        mandatory: false,
186
+                        link: 'clubs/' + this.clubId,
187
+                        mandatory: true,
188
                     }
188
                     }
189
                 ]}
189
                 ]}
190
                 renderFunction={this.getScreen}
190
                 renderFunction={this.getScreen}

+ 1
- 1
src/screens/Proxiwash/ProxiwashScreen.js View File

235
     }
235
     }
236
 
236
 
237
     /**
237
     /**
238
-     * Sets the given data as the watchlist
238
+     * Sets the given fetchedData as the watchlist
239
      *
239
      *
240
      * @param data
240
      * @param data
241
      */
241
      */

+ 0
- 1
src/screens/ScannerScreen.js View File

42
     updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"});
42
     updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"});
43
 
43
 
44
     handleCodeScanned = ({type, data}) => {
44
     handleCodeScanned = ({type, data}) => {
45
-
46
         if (!URLHandler.isUrlValid(data))
45
         if (!URLHandler.isUrlValid(data))
47
             this.showErrorDialog();
46
             this.showErrorDialog();
48
         else {
47
         else {

+ 14
- 12
src/utils/URLHandler.js View File

4
 
4
 
5
 export default class URLHandler {
5
 export default class URLHandler {
6
 
6
 
7
+    static CLUB_INFO_URL_PATH = "club";
8
+    static EVENT_INFO_URL_PATH = "event";
9
+
7
     static CLUB_INFO_ROUTE = "club-information";
10
     static CLUB_INFO_ROUTE = "club-information";
8
     static EVENT_INFO_ROUTE = "planning-information";
11
     static EVENT_INFO_ROUTE = "planning-information";
9
 
12
 
16
     }
19
     }
17
 
20
 
18
     listen() {
21
     listen() {
19
-        console.log(Linking.makeUrl('main/home/club-information', {clubId: 1}));
20
         Linking.addEventListener('url', this.onUrl);
22
         Linking.addEventListener('url', this.onUrl);
21
         Linking.parseInitialURLAsync().then(this.onInitialUrl);
23
         Linking.parseInitialURLAsync().then(this.onInitialUrl);
22
     }
24
     }
34
     };
36
     };
35
 
37
 
36
     static getUrlData({path, queryParams}: Object) {
38
     static getUrlData({path, queryParams}: Object) {
39
+        console.log(path);
37
         let data = null;
40
         let data = null;
38
         if (path !== null) {
41
         if (path !== null) {
39
-            let pathArray = path.split('/');
40
-            if (URLHandler.isClubInformationLink(pathArray))
42
+            if (URLHandler.isClubInformationLink(path))
41
                 data = URLHandler.generateClubInformationData(queryParams);
43
                 data = URLHandler.generateClubInformationData(queryParams);
42
-            else if (URLHandler.isPlanningInformationLink(pathArray))
44
+            else if (URLHandler.isPlanningInformationLink(path))
43
                 data = URLHandler.generatePlanningInformationData(queryParams);
45
                 data = URLHandler.generatePlanningInformationData(queryParams);
44
         }
46
         }
45
         return data;
47
         return data;
49
         return this.getUrlData(Linking.parse(url)) !== null;
51
         return this.getUrlData(Linking.parse(url)) !== null;
50
     }
52
     }
51
 
53
 
52
-    static isClubInformationLink(pathArray: Array<string>) {
53
-        return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "club-information";
54
+    static isClubInformationLink(path: string) {
55
+        return path === URLHandler.CLUB_INFO_URL_PATH;
54
     }
56
     }
55
 
57
 
56
-    static isPlanningInformationLink(pathArray: Array<string>) {
57
-        return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "planning-information";
58
+    static isPlanningInformationLink(path: string) {
59
+        return path === URLHandler.EVENT_INFO_URL_PATH;
58
     }
60
     }
59
 
61
 
60
     static generateClubInformationData(params: Object): Object | null {
62
     static generateClubInformationData(params: Object): Object | null {
61
-        if (params !== undefined && params.clubId !== undefined) {
62
-            let id = parseInt(params.clubId);
63
+        if (params !== undefined && params.id !== undefined) {
64
+            let id = parseInt(params.id);
63
             if (!isNaN(id)) {
65
             if (!isNaN(id)) {
64
                 return {route: URLHandler.CLUB_INFO_ROUTE, data: {clubId: id}};
66
                 return {route: URLHandler.CLUB_INFO_ROUTE, data: {clubId: id}};
65
             }
67
             }
68
     }
70
     }
69
 
71
 
70
     static generatePlanningInformationData(params: Object): Object | null {
72
     static generatePlanningInformationData(params: Object): Object | null {
71
-        if (params !== undefined && params.eventId !== undefined) {
72
-            let id = parseInt(params.eventId);
73
+        if (params !== undefined && params.id !== undefined) {
74
+            let id = parseInt(params.id);
73
             if (!isNaN(id)) {
75
             if (!isNaN(id)) {
74
                 return {route: URLHandler.EVENT_INFO_ROUTE, data: {eventId: id}};
76
                 return {route: URLHandler.EVENT_INFO_ROUTE, data: {eventId: id}};
75
             }
77
             }

+ 2
- 1
translations/en.json View File

3
     "home": "Home",
3
     "home": "Home",
4
     "planning": "Planning",
4
     "planning": "Planning",
5
     "planningDisplayScreen": "Event details",
5
     "planningDisplayScreen": "Event details",
6
+    "clubDisplayScreen": "Club details",
6
     "proxiwash": "Proxiwash",
7
     "proxiwash": "Proxiwash",
7
     "proximo": "Proximo",
8
     "proximo": "Proximo",
8
     "proximoArticles": "Articles",
9
     "proximoArticles": "Articles",
253
   "errors": {
254
   "errors": {
254
     "title": "Error!",
255
     "title": "Error!",
255
     "badCredentials": "Email or password invalid.",
256
     "badCredentials": "Email or password invalid.",
256
-    "badToken": "Session expired, please login again.",
257
+    "badToken": "You are not logged in. Please login and try again.",
257
     "noConsent": "You did not give your consent for data processing to the Amicale.",
258
     "noConsent": "You did not give your consent for data processing to the Amicale.",
258
     "badInput": "Invalid input. Please try again.",
259
     "badInput": "Invalid input. Please try again.",
259
     "forbidden": "You do not have access to this data.",
260
     "forbidden": "You do not have access to this data.",

+ 2
- 1
translations/fr.json View File

3
     "home": "Accueil",
3
     "home": "Accueil",
4
     "planning": "Planning",
4
     "planning": "Planning",
5
     "planningDisplayScreen": "Détails",
5
     "planningDisplayScreen": "Détails",
6
+    "clubDisplayScreen": "Détails",
6
     "proxiwash": "Proxiwash",
7
     "proxiwash": "Proxiwash",
7
     "proximo": "Proximo",
8
     "proximo": "Proximo",
8
     "proximoArticles": "Articles",
9
     "proximoArticles": "Articles",
253
   "errors": {
254
   "errors": {
254
     "title": "Erreur !",
255
     "title": "Erreur !",
255
     "badCredentials": "Email ou mot de passe invalide.",
256
     "badCredentials": "Email ou mot de passe invalide.",
256
-    "badToken": "Session expirée, merci de vous reconnecter.",
257
+    "badToken": "Vous n'êtes pas connecté. Merci de vous connecter puis réessayez.",
257
     "noConsent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
258
     "noConsent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
258
     "badInput": "Entrée invalide. Merci de réessayer.",
259
     "badInput": "Entrée invalide. Merci de réessayer.",
259
     "forbidden": "Vous n'avez pas accès à cette information.",
260
     "forbidden": "Vous n'avez pas accès à cette information.",

Loading…
Cancel
Save