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-safe-area-context": "0.7.3",
"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": {
"@babel/cli": "^7.8.4",

View file

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

View file

@ -1,11 +1,11 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {StyleSheet, View} from 'react-native';
import i18n from "i18n-js";
import DashboardItem from "../components/Home/EventDashboardItem";
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 SquareDashboardItem from "../components/Home/SmallDashboardItem";
import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardItem";
@ -467,9 +467,12 @@ class HomeScreen extends React.Component<Props> {
this.getDashboardItem(item) : this.getFeedItem(item));
}
openScanner = () => this.props.navigation.navigate("scanner");
render() {
const nav = this.props.navigation;
return (
<View>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
@ -477,8 +480,23 @@ class HomeScreen extends React.Component<Props> {
refreshOnFocus={true}
fetchUrl={DATA_URL}
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);

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) => {
let data = this.getUrlData(Linking.parse(url));
let data = URLHandler.getUrlData(Linking.parse(url));
if (data !== null)
this.onDetectURL(data);
};
onInitialUrl = ({path, queryParams}: Object) => {
let data = this.getUrlData({path, queryParams});
let data = URLHandler.getUrlData({path, queryParams});
if (data !== null)
this.onInitialURLParsed(data);
};
getUrlData({path, queryParams}: Object) {
static getUrlData({path, queryParams}: Object) {
let data = null;
if (path !== null) {
let pathArray = path.split('/');
if (this.isClubInformationLink(pathArray))
data = this.generateClubInformationData(queryParams);
else if (this.isPlanningInformationLink(pathArray))
data = this.generatePlanningInformationData(queryParams);
if (URLHandler.isClubInformationLink(pathArray))
data = URLHandler.generateClubInformationData(queryParams);
else if (URLHandler.isPlanningInformationLink(pathArray))
data = URLHandler.generatePlanningInformationData(queryParams);
}
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";
}
isPlanningInformationLink(pathArray: Array<string>) {
static isPlanningInformationLink(pathArray: Array<string>) {
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) {
let id = parseInt(params.clubId);
if (!isNaN(id)) {
@ -63,7 +67,7 @@ export default class URLHandler {
return null;
}
generatePlanningInformationData(params: Object): Object | null {
static generatePlanningInformationData(params: Object): Object | null {
if (params !== undefined && params.eventId !== undefined) {
let id = parseInt(params.eventId);
if (!isNaN(id)) {

View file

@ -2,9 +2,10 @@
"screens": {
"home": "Home",
"planning": "Planning",
"planningDisplayScreen": "Event Details",
"planningDisplayScreen": "Event details",
"proxiwash": "Proxiwash",
"proximo": "Proximo",
"proximoArticles": "Articles",
"menuSelf": "RU Menu",
"settings": "Settings",
"availableRooms": "Available rooms",
@ -16,7 +17,8 @@
"login": "Login",
"logout": "Logout",
"profile": "Profile",
"vote": "Elections"
"vote": "Elections",
"scanner": "Scanotron 3000"
},
"sidenav": {
"divider1": "Student websites",
@ -224,6 +226,10 @@
"membershipPayed": "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": {
"title": "Amicale account",
"subtitle": "Please enter your credentials",

View file

@ -5,6 +5,7 @@
"planningDisplayScreen": "Détails",
"proxiwash": "Proxiwash",
"proximo": "Proximo",
"proximoArticles": "Articles",
"menuSelf": "Menu du RU",
"settings": "Paramètres",
"availableRooms": "Salles dispo",
@ -16,7 +17,8 @@
"login": "Se Connecter",
"logout": "Se Déconnecter",
"profile": "Profil",
"vote": "Élections"
"vote": "Élections",
"scanner": "Scanotron 3000"
},
"sidenav": {
"divider1": "Sites étudiants",
@ -224,6 +226,10 @@
"membershipPayed": "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": {
"title": "Compte Amicale",
"subtitle": "Entrez vos identifiants",