Added qr code scanner screen

This commit is contained in:
Arnaud Vergnet 2020-04-08 15:47:40 +02:00
parent 8e2d1c7a2b
commit 96e9da162e
7 changed files with 264 additions and 25 deletions

View file

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

View file

@ -17,6 +17,7 @@ import HeaderButton from "../components/Custom/HeaderButton";
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import i18n from "i18n-js"; import i18n from "i18n-js";
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen"; import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
import ScannerScreen from "../screens/ScannerScreen";
const TAB_ICONS = { const TAB_ICONS = {
@ -62,7 +63,7 @@ function ProximoStackComponent() {
<ProximoStack.Screen <ProximoStack.Screen
name="proximo-list" name="proximo-list"
options={{ options={{
title: 'Articles' title: i18n.t('screens.proximoArticles')
}} }}
component={ProximoListScreen} component={ProximoListScreen}
/> />
@ -70,7 +71,7 @@ function ProximoStackComponent() {
name="proximo-about" name="proximo-about"
component={ProximoAboutScreen} component={ProximoAboutScreen}
options={{ options={{
title: 'Proximo', title: i18n.t('screens.proximo'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
}} }}
/> />
@ -93,7 +94,7 @@ function ProxiwashStackComponent() {
options={({navigation}) => { options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation); const openDrawer = getDrawerButton.bind(this, navigation);
return { return {
title: 'Proxiwash', title: i18n.t('screens.proxiwash'),
headerLeft: openDrawer headerLeft: openDrawer
}; };
}} }}
@ -102,7 +103,7 @@ function ProxiwashStackComponent() {
name="proxiwash-about" name="proxiwash-about"
component={ProxiwashAboutScreen} component={ProxiwashAboutScreen}
options={{ options={{
title: 'Proxiwash', title: i18n.t('screens.proxiwash'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
}} }}
/> />
@ -125,7 +126,7 @@ function PlanningStackComponent() {
options={({navigation}) => { options={({navigation}) => {
const openDrawer = getDrawerButton.bind(this, navigation); const openDrawer = getDrawerButton.bind(this, navigation);
return { return {
title: 'Planning', title: i18n.t('screens.planning'),
headerLeft: openDrawer headerLeft: openDrawer
}; };
}} }}
@ -134,7 +135,7 @@ function PlanningStackComponent() {
name="planning-information" name="planning-information"
component={PlanningDisplayScreen} component={PlanningDisplayScreen}
options={{ options={{
title: 'Details', title: i18n.t('screens.planningDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
}} }}
/> />
@ -171,7 +172,7 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
name="planning-information" name="planning-information"
component={PlanningDisplayScreen} component={PlanningDisplayScreen}
options={{ options={{
title: 'Details', title: i18n.t('screens.planningDisplayScreen'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
}} }}
/> />
@ -180,7 +181,17 @@ function HomeStackComponent(initialRoute: string | null, defaultData: Object) {
component={ClubDisplayScreen} component={ClubDisplayScreen}
options={({navigation}) => { options={({navigation}) => {
return { return {
title: "", title: '',
...TransitionPresets.ModalSlideFromBottomIOS,
};
}}
/>
<HomeStack.Screen
name="scanner"
component={ScannerScreen}
options={({navigation}) => {
return {
title: i18n.t('screens.scanner'),
...TransitionPresets.ModalSlideFromBottomIOS, ...TransitionPresets.ModalSlideFromBottomIOS,
}; };
}} }}

View file

@ -1,11 +1,11 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import i18n from "i18n-js"; import i18n from "i18n-js";
import DashboardItem from "../components/Home/EventDashboardItem"; import DashboardItem from "../components/Home/EventDashboardItem";
import WebSectionList from "../components/Lists/WebSectionList"; import WebSectionList from "../components/Lists/WebSectionList";
import {Text, withTheme} from 'react-native-paper'; import {FAB, Text, withTheme} from 'react-native-paper';
import FeedItem from "../components/Home/FeedItem"; import FeedItem from "../components/Home/FeedItem";
import SquareDashboardItem from "../components/Home/SmallDashboardItem"; import SquareDashboardItem from "../components/Home/SmallDashboardItem";
import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardItem"; import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardItem";
@ -467,9 +467,12 @@ class HomeScreen extends React.Component<Props> {
this.getDashboardItem(item) : this.getFeedItem(item)); this.getDashboardItem(item) : this.getFeedItem(item));
} }
openScanner = () => this.props.navigation.navigate("scanner");
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;
return ( return (
<View>
<WebSectionList <WebSectionList
createDataset={this.createDataset} createDataset={this.createDataset}
navigation={nav} navigation={nav}
@ -477,8 +480,23 @@ class HomeScreen extends React.Component<Props> {
refreshOnFocus={true} refreshOnFocus={true}
fetchUrl={DATA_URL} fetchUrl={DATA_URL}
renderItem={this.getRenderItem}/> renderItem={this.getRenderItem}/>
<FAB
style={styles.fab}
icon="qrcode-scan"
onPress={this.openScanner}
/>
</View>
); );
} }
} }
const styles = StyleSheet.create({
fab: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
},
});
export default withTheme(HomeScreen); export default withTheme(HomeScreen);

View file

@ -0,0 +1,192 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from "react-native";
import {Text, withTheme} from 'react-native-paper';
import {BarCodeScanner} from "expo-barcode-scanner";
import {Camera} from 'expo-camera';
import URLHandler from "../utils/URLHandler";
import {Linking} from "expo";
import AlertDialog from "../components/Dialog/AlertDialog";
import i18n from 'i18n-js';
type Props = {};
type State = {
hasPermission: boolean,
scanned: boolean,
dialogVisible: boolean,
};
class ScannerScreen extends React.Component<Props, State> {
state = {
hasPermission: false,
scanned: false,
dialogVisible: false,
};
constructor() {
super();
}
componentDidMount() {
Camera.requestPermissionsAsync().then(this.updatePermissionStatus);
}
updatePermissionStatus = ({status}) => this.setState({hasPermission: status === "granted"});
handleCodeScanned = ({type, data}) => {
this.setState({scanned: true});
if (!URLHandler.isUrlValid(data))
this.showErrorDialog();
else
Linking.openURL(data);
};
getPermissionScreen() {
return <Text>PLS</Text>
}
getOverlay() {
return (
<View style={{flex: 1}}>
<View style={{flex: 1}}>
<View style={{...overlayBackground, top: 0, height: '10%', width: '80%', left: '10%'}}/>
<View style={{...overlayBackground, left: 0, width: '10%', height: '100%'}}/>
<View style={{...overlayBackground, right: 0, width: '10%', height: '100%'}}/>
<View style={{...overlayBackground, bottom: 0, height: '10%', width: '80%', left: '10%'}}/>
</View>
<View style={styles.overlayTopLeft}>
<View style={{...overlayHorizontalLineStyle, top: 0}}/>
<View style={{...overlayVerticalLineStyle, left: 0}}/>
</View>
<View style={styles.overlayTopRight}>
<View style={{...overlayHorizontalLineStyle, top: 0}}/>
<View style={{...overlayVerticalLineStyle, right: 0}}/>
</View>
<View style={styles.overlayBottomLeft}>
<View style={{...overlayHorizontalLineStyle, bottom: 0}}/>
<View style={{...overlayVerticalLineStyle, left: 0}}/>
</View>
<View style={styles.overlayBottomRight}>
<View style={{...overlayHorizontalLineStyle, bottom: 0}}/>
<View style={{...overlayVerticalLineStyle, right: 0}}/>
</View>
</View>
);
}
showErrorDialog() {
this.setState({dialogVisible: true});
}
onDialogDismiss = () => this.setState({
dialogVisible: false,
scanned: false,
});
getScanner() {
return (
<View style={styles.cameraContainer}>
<AlertDialog
visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss}
title={i18n.t("scannerScreen.errorTitle")}
message={i18n.t("scannerScreen.errorMessage")}
/>
<Camera
onBarCodeScanned={this.state.scanned ? undefined : this.handleCodeScanned}
type={Camera.Constants.Type.back}
barCodeScannerSettings={{
barCodeTypes: [BarCodeScanner.Constants.BarCodeType.qr],
}}
style={StyleSheet.absoluteFill}
ratio={'1:1'}
>
{this.getOverlay()}
</Camera>
</View>
);
}
render() {
return (
<View style={styles.container}>
{this.state.hasPermission
? this.getScanner()
: this.getPermissionScreen()
}
</View>
);
}
}
const borderOffset = '10%';
const overlayBoxStyle = {
position: 'absolute',
width: 25,
height: 25,
};
const overlayLineStyle = {
position: 'absolute',
backgroundColor: "#fff",
borderRadius: 2,
};
const overlayHorizontalLineStyle = {
...overlayLineStyle,
width: '100%',
height: 5,
};
const overlayVerticalLineStyle = {
...overlayLineStyle,
height: '100%',
width: 5,
};
const overlayBackground = {
backgroundColor: "rgba(0,0,0,0.47)",
position: "absolute",
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000000' // the rock-solid workaround
},
cameraContainer: {
marginTop: 'auto',
marginBottom: 'auto',
aspectRatio: 1,
width: '100%',
},
overlayTopLeft: {
...overlayBoxStyle,
top: borderOffset,
left: borderOffset,
},
overlayTopRight: {
...overlayBoxStyle,
top: borderOffset,
right: borderOffset,
},
overlayBottomLeft: {
...overlayBoxStyle,
bottom: borderOffset,
left: borderOffset,
},
overlayBottomRight: {
...overlayBoxStyle,
bottom: borderOffset,
right: borderOffset,
},
});
export default withTheme(ScannerScreen);

View file

@ -22,38 +22,42 @@ export default class URLHandler {
} }
onUrl = ({url}: Object) => { onUrl = ({url}: Object) => {
let data = this.getUrlData(Linking.parse(url)); let data = URLHandler.getUrlData(Linking.parse(url));
if (data !== null) if (data !== null)
this.onDetectURL(data); this.onDetectURL(data);
}; };
onInitialUrl = ({path, queryParams}: Object) => { onInitialUrl = ({path, queryParams}: Object) => {
let data = this.getUrlData({path, queryParams}); let data = URLHandler.getUrlData({path, queryParams});
if (data !== null) if (data !== null)
this.onInitialURLParsed(data); this.onInitialURLParsed(data);
}; };
getUrlData({path, queryParams}: Object) { static getUrlData({path, queryParams}: Object) {
let data = null; let data = null;
if (path !== null) { if (path !== null) {
let pathArray = path.split('/'); let pathArray = path.split('/');
if (this.isClubInformationLink(pathArray)) if (URLHandler.isClubInformationLink(pathArray))
data = this.generateClubInformationData(queryParams); data = URLHandler.generateClubInformationData(queryParams);
else if (this.isPlanningInformationLink(pathArray)) else if (URLHandler.isPlanningInformationLink(pathArray))
data = this.generatePlanningInformationData(queryParams); data = URLHandler.generatePlanningInformationData(queryParams);
} }
return data; return data;
} }
isClubInformationLink(pathArray: Array<string>) { static isUrlValid(url: string) {
return this.getUrlData(Linking.parse(url)) !== null;
}
static isClubInformationLink(pathArray: Array<string>) {
return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "club-information"; return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "club-information";
} }
isPlanningInformationLink(pathArray: Array<string>) { static isPlanningInformationLink(pathArray: Array<string>) {
return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "planning-information"; return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "planning-information";
} }
generateClubInformationData(params: Object): Object | null { static generateClubInformationData(params: Object): Object | null {
if (params !== undefined && params.clubId !== undefined) { if (params !== undefined && params.clubId !== undefined) {
let id = parseInt(params.clubId); let id = parseInt(params.clubId);
if (!isNaN(id)) { if (!isNaN(id)) {
@ -63,7 +67,7 @@ export default class URLHandler {
return null; return null;
} }
generatePlanningInformationData(params: Object): Object | null { static generatePlanningInformationData(params: Object): Object | null {
if (params !== undefined && params.eventId !== undefined) { if (params !== undefined && params.eventId !== undefined) {
let id = parseInt(params.eventId); let id = parseInt(params.eventId);
if (!isNaN(id)) { if (!isNaN(id)) {

View file

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

View file

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