Browse Source

Improved app links and error handling on qr code opening

Arnaud Vergnet 1 year ago
parent
commit
71f39a64cc

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

@@ -1,14 +1,12 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import {withTheme} from 'react-native-paper';
5 4
 import ConnectionManager, {ERROR_TYPE} from "../../managers/ConnectionManager";
6 5
 import ErrorView from "../Custom/ErrorView";
7 6
 import BasicLoadingScreen from "../Custom/BasicLoadingScreen";
8 7
 
9 8
 type Props = {
10 9
     navigation: Object,
11
-    theme: Object,
12 10
     links: Array<{link: string, mandatory: boolean}>,
13 11
     renderFunction: Function,
14 12
 }
@@ -25,24 +23,35 @@ class AuthenticatedScreen extends React.Component<Props, State> {
25 23
 
26 24
     currentUserToken: string | null;
27 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 30
         super(props);
34
-        this.colors = props.theme.colors;
35 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 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 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 55
     fetchData = () => {
47 56
         if (!this.state.loading)
48 57
             this.setState({loading: true});
@@ -50,39 +59,51 @@ class AuthenticatedScreen extends React.Component<Props, State> {
50 59
             for (let i = 0; i < this.props.links.length; i++) {
51 60
                 this.connectionManager.authenticatedRequest(this.props.links[i].link, null, null)
52 61
                     .then((data) => {
53
-                        this.onFinishedLoading(data, i, -1);
62
+                        this.onRequestFinished(data, i, -1);
54 63
                     })
55 64
                     .catch((error) => {
56
-                        this.onFinishedLoading(null, i, error);
65
+                        this.onRequestFinished(null, i, error);
57 66
                     });
58 67
             }
59
-
60 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 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 103
     allRequestsFinished() {
83 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 107
                 finished = false;
87 108
                 break;
88 109
             }
@@ -90,10 +111,17 @@ class AuthenticatedScreen extends React.Component<Props, State> {
90 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 121
     allRequestsValid() {
94 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 125
                 valid = false;
98 126
                 break;
99 127
             }
@@ -101,15 +129,40 @@ class AuthenticatedScreen extends React.Component<Props, State> {
101 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 153
     getErrorRender() {
105 154
         return (
106 155
             <ErrorView
107
-                errorCode={this.errorCode}
156
+                {...this.props}
157
+                errorCode={this.getError()}
108 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 166
     reload() {
114 167
         this.fetchData();
115 168
     }
@@ -119,10 +172,10 @@ class AuthenticatedScreen extends React.Component<Props, State> {
119 172
             this.state.loading
120 173
                 ? <BasicLoadingScreen/>
121 174
                 : (this.allRequestsValid()
122
-                ? this.props.renderFunction(this.data)
175
+                ? this.props.renderFunction(this.fetchedData)
123 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,6 +8,7 @@ import i18n from 'i18n-js';
8 8
 import {ERROR_TYPE} from "../../managers/ConnectionManager";
9 9
 
10 10
 type Props = {
11
+    navigation: Object,
11 12
     errorCode: number,
12 13
     onRefresh: Function,
13 14
 }
@@ -23,6 +24,8 @@ class ErrorView extends React.PureComponent<Props, State> {
23 24
     message: string;
24 25
     icon: string;
25 26
 
27
+    showLoginButton: boolean;
28
+
26 29
     state = {
27 30
         refreshing: false,
28 31
     };
@@ -33,6 +36,7 @@ class ErrorView extends React.PureComponent<Props, State> {
33 36
     }
34 37
 
35 38
     generateMessage() {
39
+        this.showLoginButton = false;
36 40
         switch (this.props.errorCode) {
37 41
             case ERROR_TYPE.BAD_CREDENTIALS:
38 42
                 this.message = i18n.t("errors.badCredentials");
@@ -41,6 +45,7 @@ class ErrorView extends React.PureComponent<Props, State> {
41 45
             case ERROR_TYPE.BAD_TOKEN:
42 46
                 this.message = i18n.t("errors.badToken");
43 47
                 this.icon = "account-alert-outline";
48
+                this.showLoginButton = true;
44 49
                 break;
45 50
             case ERROR_TYPE.NO_CONSENT:
46 51
                 this.message = i18n.t("errors.noConsent");
@@ -69,6 +74,30 @@ class ErrorView extends React.PureComponent<Props, State> {
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 101
     render() {
73 102
         this.generateMessage();
74 103
         return (
@@ -89,14 +118,9 @@ class ErrorView extends React.PureComponent<Props, State> {
89 118
                     }}>
90 119
                         {this.message}
91 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 124
                 </View>
101 125
             </View>
102 126
         );

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

@@ -194,6 +194,7 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
194 194
                     ListEmptyComponent={this.state.refreshing
195 195
                         ? <BasicLoadingScreen/>
196 196
                         : <ErrorView
197
+                            {...this.props}
197 198
                             errorCode={ERROR_TYPE.CONNECTION_ERROR}
198 199
                             onRefresh={this.onRefresh}/>
199 200
                     }

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

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

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

@@ -33,7 +33,6 @@ class DebugScreen extends React.Component<Props, State> {
33 33
         this.onModalRef = this.onModalRef.bind(this);
34 34
         this.colors = props.theme.colors;
35 35
         let copy = {...AsyncStorageManager.getInstance().preferences};
36
-        console.log(copy);
37 36
         let currentPreferences = [];
38 37
         Object.values(copy).map((object) => {
39 38
             currentPreferences.push(object);

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

@@ -39,7 +39,7 @@ const FakeClub = {
39 39
 };
40 40
 
41 41
 /**
42
- * Class defining a planning event information page.
42
+ * Class defining a club event information page.
43 43
  * If called with data and categories navigation parameters, will use those to display the data.
44 44
  * If called with clubId parameter, will fetch the information on the server
45 45
  */
@@ -61,7 +61,6 @@ class ClubDisplayScreen extends React.Component<Props, State> {
61 61
         super(props);
62 62
         this.colors = props.theme.colors;
63 63
 
64
-        console.log(this.props.route.params);
65 64
         if (this.props.route.params.data !== undefined && this.props.route.params.categories !== undefined) {
66 65
             this.displayData = this.props.route.params.data;
67 66
             this.categories = this.props.route.params.categories;
@@ -72,7 +71,6 @@ class ClubDisplayScreen extends React.Component<Props, State> {
72 71
             this.categories = null;
73 72
             this.clubId = this.props.route.params.clubId;
74 73
             this.shouldFetchData = true;
75
-            console.log(this.clubId);
76 74
         }
77 75
     }
78 76
 
@@ -135,6 +133,8 @@ class ClubDisplayScreen extends React.Component<Props, State> {
135 133
     }
136 134
 
137 135
     getScreen = (data: Object) => {
136
+        console.log('fetchedData passed to screen:');
137
+        console.log(data);
138 138
         data = FakeClub;
139 139
         this.updateHeaderTitle(data);
140 140
 
@@ -183,8 +183,8 @@ class ClubDisplayScreen extends React.Component<Props, State> {
183 183
                 {...this.props}
184 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 190
                 renderFunction={this.getScreen}

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

@@ -235,7 +235,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
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 240
      * @param data
241 241
      */

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

@@ -42,7 +42,6 @@ class ScannerScreen extends React.Component<Props, State> {
42 42
     updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"});
43 43
 
44 44
     handleCodeScanned = ({type, data}) => {
45
-
46 45
         if (!URLHandler.isUrlValid(data))
47 46
             this.showErrorDialog();
48 47
         else {

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

@@ -4,6 +4,9 @@ import {Linking} from 'expo';
4 4
 
5 5
 export default class URLHandler {
6 6
 
7
+    static CLUB_INFO_URL_PATH = "club";
8
+    static EVENT_INFO_URL_PATH = "event";
9
+
7 10
     static CLUB_INFO_ROUTE = "club-information";
8 11
     static EVENT_INFO_ROUTE = "planning-information";
9 12
 
@@ -16,7 +19,6 @@ export default class URLHandler {
16 19
     }
17 20
 
18 21
     listen() {
19
-        console.log(Linking.makeUrl('main/home/club-information', {clubId: 1}));
20 22
         Linking.addEventListener('url', this.onUrl);
21 23
         Linking.parseInitialURLAsync().then(this.onInitialUrl);
22 24
     }
@@ -34,12 +36,12 @@ export default class URLHandler {
34 36
     };
35 37
 
36 38
     static getUrlData({path, queryParams}: Object) {
39
+        console.log(path);
37 40
         let data = null;
38 41
         if (path !== null) {
39
-            let pathArray = path.split('/');
40
-            if (URLHandler.isClubInformationLink(pathArray))
42
+            if (URLHandler.isClubInformationLink(path))
41 43
                 data = URLHandler.generateClubInformationData(queryParams);
42
-            else if (URLHandler.isPlanningInformationLink(pathArray))
44
+            else if (URLHandler.isPlanningInformationLink(path))
43 45
                 data = URLHandler.generatePlanningInformationData(queryParams);
44 46
         }
45 47
         return data;
@@ -49,17 +51,17 @@ export default class URLHandler {
49 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 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 65
             if (!isNaN(id)) {
64 66
                 return {route: URLHandler.CLUB_INFO_ROUTE, data: {clubId: id}};
65 67
             }
@@ -68,8 +70,8 @@ export default class URLHandler {
68 70
     }
69 71
 
70 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 75
             if (!isNaN(id)) {
74 76
                 return {route: URLHandler.EVENT_INFO_ROUTE, data: {eventId: id}};
75 77
             }

+ 2
- 1
translations/en.json View File

@@ -3,6 +3,7 @@
3 3
     "home": "Home",
4 4
     "planning": "Planning",
5 5
     "planningDisplayScreen": "Event details",
6
+    "clubDisplayScreen": "Club details",
6 7
     "proxiwash": "Proxiwash",
7 8
     "proximo": "Proximo",
8 9
     "proximoArticles": "Articles",
@@ -253,7 +254,7 @@
253 254
   "errors": {
254 255
     "title": "Error!",
255 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 258
     "noConsent": "You did not give your consent for data processing to the Amicale.",
258 259
     "badInput": "Invalid input. Please try again.",
259 260
     "forbidden": "You do not have access to this data.",

+ 2
- 1
translations/fr.json View File

@@ -3,6 +3,7 @@
3 3
     "home": "Accueil",
4 4
     "planning": "Planning",
5 5
     "planningDisplayScreen": "Détails",
6
+    "clubDisplayScreen": "Détails",
6 7
     "proxiwash": "Proxiwash",
7 8
     "proximo": "Proximo",
8 9
     "proximoArticles": "Articles",
@@ -253,7 +254,7 @@
253 254
   "errors": {
254 255
     "title": "Erreur !",
255 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 258
     "noConsent": "Vous n'avez pas donné votre consentement pour l'utilisation de vos données personnelles.",
258 259
     "badInput": "Entrée invalide. Merci de réessayer.",
259 260
     "forbidden": "Vous n'avez pas accès à cette information.",

Loading…
Cancel
Save