diff --git a/package.json b/package.json index c6c99ab..1a15199 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/navigation/MainTabNavigator.js b/src/navigation/MainTabNavigator.js index 041e7a9..51a2735 100644 --- a/src/navigation/MainTabNavigator.js +++ b/src/navigation/MainTabNavigator.js @@ -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() { @@ -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, + }; + }} + /> + { + return { + title: i18n.t('screens.scanner'), ...TransitionPresets.ModalSlideFromBottomIOS, }; }} diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js index 30bf86c..f753ab9 100644 --- a/src/screens/HomeScreen.js +++ b/src/screens/HomeScreen.js @@ -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 { this.getDashboardItem(item) : this.getFeedItem(item)); } + openScanner = () => this.props.navigation.navigate("scanner"); + render() { const nav = this.props.navigation; return ( + { refreshOnFocus={true} fetchUrl={DATA_URL} renderItem={this.getRenderItem}/> + + ); } } +const styles = StyleSheet.create({ + fab: { + position: 'absolute', + margin: 16, + right: 0, + bottom: 0, + }, +}); + export default withTheme(HomeScreen); diff --git a/src/screens/ScannerScreen.js b/src/screens/ScannerScreen.js new file mode 100644 index 0000000..b46d6dd --- /dev/null +++ b/src/screens/ScannerScreen.js @@ -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 { + + 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 PLS + } + + getOverlay() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + + showErrorDialog() { + this.setState({dialogVisible: true}); + } + + onDialogDismiss = () => this.setState({ + dialogVisible: false, + scanned: false, + }); + + getScanner() { + return ( + + + + {this.getOverlay()} + + + ); + } + + render() { + return ( + + {this.state.hasPermission + ? this.getScanner() + : this.getPermissionScreen() + } + + ); + } +} + +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); diff --git a/src/utils/URLHandler.js b/src/utils/URLHandler.js index ae9f41b..b907057 100644 --- a/src/utils/URLHandler.js +++ b/src/utils/URLHandler.js @@ -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) { + static isUrlValid(url: string) { + return this.getUrlData(Linking.parse(url)) !== null; + } + + static isClubInformationLink(pathArray: Array) { return pathArray[0] === "main" && pathArray[1] === "home" && pathArray[2] === "club-information"; } - isPlanningInformationLink(pathArray: Array) { + static isPlanningInformationLink(pathArray: Array) { 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)) { diff --git a/translations/en.json b/translations/en.json index 6e1658c..5948d81 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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", diff --git a/translations/fr.json b/translations/fr.json index 0834878..96e5f6d 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -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",