Browse Source

Added qr code scanner screen

Arnaud Vergnet 4 years ago
parent
commit
96e9da162e

+ 3
- 1
package.json View File

@@ -49,7 +49,9 @@
49 49
     "react-native-render-html": "^4.1.2",
50 50
     "react-native-safe-area-context": "0.7.3",
51 51
     "react-native-screens": "~2.2.0",
52
-    "react-native-webview": "8.1.1"
52
+    "react-native-webview": "8.1.1",
53
+    "expo-barcode-scanner": "~8.1.0",
54
+    "expo-camera": "latest"
53 55
   },
54 56
   "devDependencies": {
55 57
     "@babel/cli": "^7.8.4",

+ 19
- 8
src/navigation/MainTabNavigator.js View File

@@ -17,6 +17,7 @@ import HeaderButton from "../components/Custom/HeaderButton";
17 17
 import {withTheme} from 'react-native-paper';
18 18
 import i18n from "i18n-js";
19 19
 import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
20
+import ScannerScreen from "../screens/ScannerScreen";
20 21
 
21 22
 
22 23
 const TAB_ICONS = {
@@ -62,7 +63,7 @@ function ProximoStackComponent() {
62 63
             <ProximoStack.Screen
63 64
                 name="proximo-list"
64 65
                 options={{
65
-                    title: 'Articles'
66
+                    title: i18n.t('screens.proximoArticles')
66 67
                 }}
67 68
                 component={ProximoListScreen}
68 69
             />
@@ -70,7 +71,7 @@ function ProximoStackComponent() {
70 71
                 name="proximo-about"
71 72
                 component={ProximoAboutScreen}
72 73
                 options={{
73
-                    title: 'Proximo',
74
+                    title: i18n.t('screens.proximo'),
74 75
                     ...TransitionPresets.ModalSlideFromBottomIOS,
75 76
                 }}
76 77
             />
@@ -93,7 +94,7 @@ function ProxiwashStackComponent() {
93 94
                 options={({navigation}) => {
94 95
                     const openDrawer = getDrawerButton.bind(this, navigation);
95 96
                     return {
96
-                        title: 'Proxiwash',
97
+                        title: i18n.t('screens.proxiwash'),
97 98
                         headerLeft: openDrawer
98 99
                     };
99 100
                 }}
@@ -102,7 +103,7 @@ function ProxiwashStackComponent() {
102 103
                 name="proxiwash-about"
103 104
                 component={ProxiwashAboutScreen}
104 105
                 options={{
105
-                    title: 'Proxiwash',
106
+                    title: i18n.t('screens.proxiwash'),
106 107
                     ...TransitionPresets.ModalSlideFromBottomIOS,
107 108
                 }}
108 109
             />
@@ -125,7 +126,7 @@ function PlanningStackComponent() {
125 126
                 options={({navigation}) => {
126 127
                     const openDrawer = getDrawerButton.bind(this, navigation);
127 128
                     return {
128
-                        title: 'Planning',
129
+                        title: i18n.t('screens.planning'),
129 130
                         headerLeft: openDrawer
130 131
                     };
131 132
                 }}
@@ -134,7 +135,7 @@ function PlanningStackComponent() {
134 135
                 name="planning-information"
135 136
                 component={PlanningDisplayScreen}
136 137
                 options={{
137
-                    title: 'Details',
138
+                    title: i18n.t('screens.planningDisplayScreen'),
138 139
                     ...TransitionPresets.ModalSlideFromBottomIOS,
139 140
                 }}
140 141
             />
@@ -171,7 +172,7 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
171 172
                 name="planning-information"
172 173
                 component={PlanningDisplayScreen}
173 174
                 options={{
174
-                    title: 'Details',
175
+                    title: i18n.t('screens.planningDisplayScreen'),
175 176
                     ...TransitionPresets.ModalSlideFromBottomIOS,
176 177
                 }}
177 178
             />
@@ -180,7 +181,17 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
180 181
                 component={ClubDisplayScreen}
181 182
                 options={({navigation}) => {
182 183
                     return {
183
-                        title: "",
184
+                        title: '',
185
+                        ...TransitionPresets.ModalSlideFromBottomIOS,
186
+                    };
187
+                }}
188
+            />
189
+            <HomeStack.Screen
190
+                name="scanner"
191
+                component={ScannerScreen}
192
+                options={({navigation}) => {
193
+                    return {
194
+                        title: i18n.t('screens.scanner'),
184 195
                         ...TransitionPresets.ModalSlideFromBottomIOS,
185 196
                     };
186 197
                 }}

+ 20
- 2
src/screens/HomeScreen.js View File

@@ -1,11 +1,11 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import {View} from 'react-native';
4
+import {StyleSheet, View} from 'react-native';
5 5
 import i18n from "i18n-js";
6 6
 import DashboardItem from "../components/Home/EventDashboardItem";
7 7
 import WebSectionList from "../components/Lists/WebSectionList";
8
-import {Text, withTheme} from 'react-native-paper';
8
+import {FAB, Text, withTheme} from 'react-native-paper';
9 9
 import FeedItem from "../components/Home/FeedItem";
10 10
 import SquareDashboardItem from "../components/Home/SmallDashboardItem";
11 11
 import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardItem";
@@ -467,9 +467,12 @@ class HomeScreen extends React.Component<Props> {
467 467
             this.getDashboardItem(item) : this.getFeedItem(item));
468 468
     }
469 469
 
470
+    openScanner = () => this.props.navigation.navigate("scanner");
471
+
470 472
     render() {
471 473
         const nav = this.props.navigation;
472 474
         return (
475
+            <View>
473 476
             <WebSectionList
474 477
                 createDataset={this.createDataset}
475 478
                 navigation={nav}
@@ -477,8 +480,23 @@ class HomeScreen extends React.Component<Props> {
477 480
                 refreshOnFocus={true}
478 481
                 fetchUrl={DATA_URL}
479 482
                 renderItem={this.getRenderItem}/>
483
+                <FAB
484
+                    style={styles.fab}
485
+                    icon="qrcode-scan"
486
+                    onPress={this.openScanner}
487
+                />
488
+            </View>
480 489
         );
481 490
     }
482 491
 }
483 492
 
493
+const styles = StyleSheet.create({
494
+    fab: {
495
+        position: 'absolute',
496
+        margin: 16,
497
+        right: 0,
498
+        bottom: 0,
499
+    },
500
+});
501
+
484 502
 export default withTheme(HomeScreen);

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

@@ -0,0 +1,192 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {StyleSheet, View} from "react-native";
5
+import {Text, withTheme} from 'react-native-paper';
6
+import {BarCodeScanner} from "expo-barcode-scanner";
7
+import {Camera} from 'expo-camera';
8
+import URLHandler from "../utils/URLHandler";
9
+import {Linking} from "expo";
10
+import AlertDialog from "../components/Dialog/AlertDialog";
11
+import i18n from 'i18n-js';
12
+
13
+type Props = {};
14
+type State = {
15
+    hasPermission: boolean,
16
+    scanned: boolean,
17
+    dialogVisible: boolean,
18
+};
19
+
20
+class ScannerScreen extends React.Component<Props, State> {
21
+
22
+    state = {
23
+        hasPermission: false,
24
+        scanned: false,
25
+        dialogVisible: false,
26
+    };
27
+
28
+    constructor() {
29
+        super();
30
+    }
31
+
32
+    componentDidMount() {
33
+        Camera.requestPermissionsAsync().then(this.updatePermissionStatus);
34
+    }
35
+
36
+    updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"});
37
+
38
+
39
+    handleCodeScanned = ({type, data}) => {
40
+        this.setState({scanned: true});
41
+        if (!URLHandler.isUrlValid(data))
42
+            this.showErrorDialog();
43
+        else
44
+            Linking.openURL(data);
45
+    };
46
+
47
+    getPermissionScreen() {
48
+        return <Text>PLS</Text>
49
+    }
50
+
51
+    getOverlay() {
52
+        return (
53
+            <View style={{flex: 1}}>
54
+                <View style={{flex: 1}}>
55
+                    <View style={{...overlayBackground, top: 0, height: '10%', width: '80%', left: '10%'}}/>
56
+                    <View style={{...overlayBackground, left: 0, width: '10%', height: '100%'}}/>
57
+                    <View style={{...overlayBackground, right: 0, width: '10%', height: '100%'}}/>
58
+                    <View style={{...overlayBackground, bottom: 0, height: '10%', width: '80%', left: '10%'}}/>
59
+                </View>
60
+
61
+                <View style={styles.overlayTopLeft}>
62
+                    <View style={{...overlayHorizontalLineStyle, top: 0}}/>
63
+                    <View style={{...overlayVerticalLineStyle, left: 0}}/>
64
+                </View>
65
+                <View style={styles.overlayTopRight}>
66
+                    <View style={{...overlayHorizontalLineStyle, top: 0}}/>
67
+                    <View style={{...overlayVerticalLineStyle, right: 0}}/>
68
+                </View>
69
+                <View style={styles.overlayBottomLeft}>
70
+                    <View style={{...overlayHorizontalLineStyle, bottom: 0}}/>
71
+                    <View style={{...overlayVerticalLineStyle, left: 0}}/>
72
+                </View>
73
+                <View style={styles.overlayBottomRight}>
74
+                    <View style={{...overlayHorizontalLineStyle, bottom: 0}}/>
75
+                    <View style={{...overlayVerticalLineStyle, right: 0}}/>
76
+                </View>
77
+            </View>
78
+        );
79
+    }
80
+
81
+    showErrorDialog() {
82
+        this.setState({dialogVisible: true});
83
+    }
84
+
85
+    onDialogDismiss = () => this.setState({
86
+        dialogVisible: false,
87
+        scanned: false,
88
+    });
89
+
90
+    getScanner() {
91
+        return (
92
+            <View style={styles.cameraContainer}>
93
+                <AlertDialog
94
+                    visible={this.state.dialogVisible}
95
+                    onDismiss={this.onDialogDismiss}
96
+                    title={i18n.t("scannerScreen.errorTitle")}
97
+                    message={i18n.t("scannerScreen.errorMessage")}
98
+                />
99
+                <Camera
100
+                    onBarCodeScanned={this.state.scanned ? undefined : this.handleCodeScanned}
101
+                    type={Camera.Constants.Type.back}
102
+                    barCodeScannerSettings={{
103
+                        barCodeTypes: [BarCodeScanner.Constants.BarCodeType.qr],
104
+                    }}
105
+                    style={StyleSheet.absoluteFill}
106
+                    ratio={'1:1'}
107
+                >
108
+                    {this.getOverlay()}
109
+                </Camera>
110
+            </View>
111
+        );
112
+    }
113
+
114
+    render() {
115
+        return (
116
+            <View style={styles.container}>
117
+                {this.state.hasPermission
118
+                    ? this.getScanner()
119
+                    : this.getPermissionScreen()
120
+                }
121
+            </View>
122
+        );
123
+    }
124
+}
125
+
126
+const borderOffset = '10%';
127
+
128
+const overlayBoxStyle = {
129
+    position: 'absolute',
130
+    width: 25,
131
+    height: 25,
132
+};
133
+
134
+const overlayLineStyle = {
135
+    position: 'absolute',
136
+    backgroundColor: "#fff",
137
+    borderRadius: 2,
138
+};
139
+
140
+const overlayHorizontalLineStyle = {
141
+    ...overlayLineStyle,
142
+    width: '100%',
143
+    height: 5,
144
+};
145
+
146
+const overlayVerticalLineStyle = {
147
+    ...overlayLineStyle,
148
+    height: '100%',
149
+    width: 5,
150
+};
151
+
152
+const overlayBackground = {
153
+    backgroundColor: "rgba(0,0,0,0.47)",
154
+    position: "absolute",
155
+};
156
+
157
+const styles = StyleSheet.create({
158
+    container: {
159
+        flex: 1,
160
+        justifyContent: 'center',
161
+        alignItems: 'center',
162
+        backgroundColor: '#000000' // the rock-solid workaround
163
+    },
164
+    cameraContainer: {
165
+        marginTop: 'auto',
166
+        marginBottom: 'auto',
167
+        aspectRatio: 1,
168
+        width: '100%',
169
+    },
170
+    overlayTopLeft: {
171
+        ...overlayBoxStyle,
172
+        top: borderOffset,
173
+        left: borderOffset,
174
+    },
175
+    overlayTopRight: {
176
+        ...overlayBoxStyle,
177
+        top: borderOffset,
178
+        right: borderOffset,
179
+    },
180
+    overlayBottomLeft: {
181
+        ...overlayBoxStyle,
182
+        bottom: borderOffset,
183
+        left: borderOffset,
184
+    },
185
+    overlayBottomRight: {
186
+        ...overlayBoxStyle,
187
+        bottom: borderOffset,
188
+        right: borderOffset,
189
+    },
190
+});
191
+
192
+export default withTheme(ScannerScreen);

+ 15
- 11
src/utils/URLHandler.js View File

@@ -22,38 +22,42 @@ export default class URLHandler {
22 22
     }
23 23
 
24 24
     onUrl = ({url}: Object) => {
25
-        let data = this.getUrlData(Linking.parse(url));
25
+        let data = URLHandler.getUrlData(Linking.parse(url));
26 26
         if (data !== null)
27 27
             this.onDetectURL(data);
28 28
     };
29 29
 
30 30
     onInitialUrl = ({path, queryParams}: Object) => {
31
-        let data = this.getUrlData({path, queryParams});
31
+        let data = URLHandler.getUrlData({path, queryParams});
32 32
         if (data !== null)
33 33
             this.onInitialURLParsed(data);
34 34
     };
35 35
 
36
-    getUrlData({path, queryParams}: Object) {
36
+    static getUrlData({path, queryParams}: Object) {
37 37
         let data = null;
38 38
         if (path !== null) {
39 39
             let pathArray = path.split('/');
40
-            if (this.isClubInformationLink(pathArray))
41
-                data = this.generateClubInformationData(queryParams);
42
-            else if (this.isPlanningInformationLink(pathArray))
43
-                data = this.generatePlanningInformationData(queryParams);
40
+            if (URLHandler.isClubInformationLink(pathArray))
41
+                data = URLHandler.generateClubInformationData(queryParams);
42
+            else if (URLHandler.isPlanningInformationLink(pathArray))
43
+                data = URLHandler.generatePlanningInformationData(queryParams);
44 44
         }
45 45
         return data;
46 46
     }
47 47
 
48
-    isClubInformationLink(pathArray: Array<string>) {
48
+    static isUrlValid(url: string) {
49
+        return this.getUrlData(Linking.parse(url)) !== null;
50
+    }
51
+
52
+    static isClubInformationLink(pathArray: Array<string>) {
49 53
         return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "club-information";
50 54
     }
51 55
 
52
-    isPlanningInformationLink(pathArray: Array<string>) {
56
+    static isPlanningInformationLink(pathArray: Array<string>) {
53 57
         return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "planning-information";
54 58
     }
55 59
 
56
-    generateClubInformationData(params: Object): Object | null {
60
+    static generateClubInformationData(params: Object): Object | null {
57 61
         if (params !== undefined && params.clubId !== undefined) {
58 62
             let id = parseInt(params.clubId);
59 63
             if (!isNaN(id)) {
@@ -63,7 +67,7 @@ export default class URLHandler {
63 67
         return null;
64 68
     }
65 69
 
66
-    generatePlanningInformationData(params: Object): Object | null {
70
+    static generatePlanningInformationData(params: Object): Object | null {
67 71
         if (params !== undefined && params.eventId !== undefined) {
68 72
             let id = parseInt(params.eventId);
69 73
             if (!isNaN(id)) {

+ 8
- 2
translations/en.json View File

@@ -2,9 +2,10 @@
2 2
   "screens": {
3 3
     "home": "Home",
4 4
     "planning": "Planning",
5
-    "planningDisplayScreen": "Event Details",
5
+    "planningDisplayScreen": "Event details",
6 6
     "proxiwash": "Proxiwash",
7 7
     "proximo": "Proximo",
8
+    "proximoArticles": "Articles",
8 9
     "menuSelf": "RU Menu",
9 10
     "settings": "Settings",
10 11
     "availableRooms": "Available rooms",
@@ -16,7 +17,8 @@
16 17
     "login": "Login",
17 18
     "logout": "Logout",
18 19
     "profile": "Profile",
19
-    "vote": "Elections"
20
+    "vote": "Elections",
21
+    "scanner": "Scanotron 3000"
20 22
   },
21 23
   "sidenav": {
22 24
     "divider1": "Student websites",
@@ -224,6 +226,10 @@
224 226
     "membershipPayed": "Payed",
225 227
     "membershipNotPayed": "Not payed"
226 228
   },
229
+  "scannerScreen": {
230
+    "errorTitle": "QR code invalid",
231
+    "errorMessage": "The QR code scanned could not be recognised, please make sure it is valid."
232
+  },
227 233
   "loginScreen": {
228 234
     "title": "Amicale account",
229 235
     "subtitle": "Please enter your credentials",

+ 7
- 1
translations/fr.json View File

@@ -5,6 +5,7 @@
5 5
     "planningDisplayScreen": "Détails",
6 6
     "proxiwash": "Proxiwash",
7 7
     "proximo": "Proximo",
8
+    "proximoArticles": "Articles",
8 9
     "menuSelf": "Menu du RU",
9 10
     "settings": "Paramètres",
10 11
     "availableRooms": "Salles dispo",
@@ -16,7 +17,8 @@
16 17
     "login": "Se Connecter",
17 18
     "logout": "Se Déconnecter",
18 19
     "profile": "Profil",
19
-    "vote": "Élections"
20
+    "vote": "Élections",
21
+    "scanner": "Scanotron 3000"
20 22
   },
21 23
   "sidenav": {
22 24
     "divider1": "Sites étudiants",
@@ -224,6 +226,10 @@
224 226
     "membershipPayed": "Payée",
225 227
     "membershipNotPayed": "Non payée"
226 228
   },
229
+  "scannerScreen": {
230
+    "errorTitle": "QR code invalide",
231
+    "errorMessage": "Le QR code scannée n'a pas été reconnu. Merci de vérifier sa validité."
232
+  },
227 233
   "loginScreen": {
228 234
     "title": "Compte Amicale",
229 235
     "subtitle": "Entrez vos identifiants",

Loading…
Cancel
Save