react-native-qrcode-scanner
Version: 
A QR code scanner for React Native.
397 lines (367 loc) • 10 kB
JavaScript
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
  StyleSheet,
  Dimensions,
  Vibration,
  Animated,
  Easing,
  View,
  Text,
  Platform,
  TouchableWithoutFeedback,
  PermissionsAndroid,
} from 'react-native';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { RNCamera as Camera } from 'react-native-camera';
const CAMERA_FLASH_MODE = Camera.Constants.FlashMode;
const CAMERA_FLASH_MODES = [
  CAMERA_FLASH_MODE.torch,
  CAMERA_FLASH_MODE.on,
  CAMERA_FLASH_MODE.off,
  CAMERA_FLASH_MODE.auto,
];
export default class QRCodeScanner extends Component {
  static propTypes = {
    onRead: PropTypes.func.isRequired,
    vibrate: PropTypes.bool,
    reactivate: PropTypes.bool,
    reactivateTimeout: PropTypes.number,
    cameraTimeout: PropTypes.number,
    fadeIn: PropTypes.bool,
    showMarker: PropTypes.bool,
    cameraType: PropTypes.oneOf(['front', 'back']),
    customMarker: PropTypes.element,
    containerStyle: PropTypes.any,
    cameraStyle: PropTypes.any,
    cameraContainerStyle: PropTypes.any,
    markerStyle: PropTypes.any,
    topViewStyle: PropTypes.any,
    bottomViewStyle: PropTypes.any,
    topContent: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
    bottomContent: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
    notAuthorizedView: PropTypes.element,
    permissionDialogTitle: PropTypes.string,
    permissionDialogMessage: PropTypes.string,
    buttonPositive: PropTypes.string,
    checkAndroid6Permissions: PropTypes.bool,
    flashMode: PropTypes.oneOf(CAMERA_FLASH_MODES),
    cameraProps: PropTypes.object,
    cameraTimeoutView: PropTypes.element,
  };
  static defaultProps = {
    onRead: () => null,
    reactivate: false,
    vibrate: true,
    reactivateTimeout: 0,
    cameraTimeout: 0,
    fadeIn: true,
    showMarker: false,
    cameraType: 'back',
    notAuthorizedView: (
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <Text
          style={{
            textAlign: 'center',
            fontSize: 16,
          }}
        >
          Camera not authorized
        </Text>
      </View>
    ),
    pendingAuthorizationView: (
      <View
        style={{
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <Text
          style={{
            textAlign: 'center',
            fontSize: 16,
          }}
        >
          ...
        </Text>
      </View>
    ),
    permissionDialogTitle: 'Info',
    permissionDialogMessage: 'Need camera permission',
    buttonPositive: 'OK',
    checkAndroid6Permissions: false,
    flashMode: CAMERA_FLASH_MODE.auto,
    cameraProps: {},
    cameraTimeoutView: (
      <View
        style={{
          flex: 0,
          alignItems: 'center',
          justifyContent: 'center',
          height: Dimensions.get('window').height,
          width: Dimensions.get('window').width,
          backgroundColor: 'black',
        }}
      >
        <Text style={{ color: 'white' }}>Tap to activate camera</Text>
      </View>
    ),
  };
  constructor(props) {
    super(props);
    this.state = {
      scanning: false,
      isCameraActivated: true,
      fadeInOpacity: new Animated.Value(0),
      isAuthorized: false,
      isAuthorizationChecked: false,
      disableVibrationByUser: false,
    };
    this.timer = null;
    this._scannerTimeout = null;
    this._handleBarCodeRead = this._handleBarCodeRead.bind(this);
  }
  componentDidMount() {
    if (Platform.OS === 'ios') {
      request(PERMISSIONS.IOS.CAMERA).then(cameraStatus => {
        this.setState({
          isAuthorized: cameraStatus === RESULTS.GRANTED,
          isAuthorizationChecked: true,
        });
      });
    } else if (
      Platform.OS === 'android' &&
      this.props.checkAndroid6Permissions
    ) {
      PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA, {
        title: this.props.permissionDialogTitle,
        message: this.props.permissionDialogMessage,
        buttonPositive: this.props.buttonPositive,
      }).then(granted => {
        const isAuthorized = granted === PermissionsAndroid.RESULTS.GRANTED;
        this.setState({ isAuthorized, isAuthorizationChecked: true });
      });
    } else {
      this.setState({ isAuthorized: true, isAuthorizationChecked: true });
    }
    if (this.props.fadeIn) {
      Animated.sequence([
        Animated.delay(1000),
        Animated.timing(this.state.fadeInOpacity, {
          toValue: 1,
          easing: Easing.inOut(Easing.quad),
          useNativeDriver: true,
        }),
      ]).start();
    }
  }
  componentWillUnmount() {
    if (this._scannerTimeout !== null) {
      clearTimeout(this._scannerTimeout);
    }
    if (this.timer !== null) {
      clearTimeout(this.timer);
    }
    this.timer = null;
    this._scannerTimeout = null;
  }
  disable() {
    this.setState({ disableVibrationByUser: true });
  }
  enable() {
    this.setState({ disableVibrationByUser: false });
  }
  _setScanning(value) {
    this.setState({ scanning: value });
  }
  _setCamera(value) {
    this.setState(
      {
        isCameraActivated: value,
        scanning: false,
        fadeInOpacity: new Animated.Value(0),
      },
      () => {
        if (value && this.props.fadeIn) {
          if (this.props.fadeIn) {
            Animated.sequence([
              Animated.delay(10),
              Animated.timing(this.state.fadeInOpacity, {
                toValue: 1,
                easing: Easing.inOut(Easing.quad),
                useNativeDriver: true,
              }),
            ]).start();
          }
        }
      }
    );
  }
  _handleBarCodeRead(e) {
    if (!this.state.scanning && !this.state.disableVibrationByUser) {
      if (this.props.vibrate) {
        Vibration.vibrate();
      }
      this._setScanning(true);
      this.props.onRead(e);
      if (this.props.reactivate) {
        this._scannerTimeout = setTimeout(
          () => this._setScanning(false),
          this.props.reactivateTimeout
        );
      }
    }
  }
  _renderTopContent() {
    if (this.props.topContent) {
      return this.props.topContent;
    }
    return null;
  }
  _renderBottomContent() {
    if (this.props.bottomContent) {
      return this.props.bottomContent;
    }
    return null;
  }
  _renderCameraMarker() {
    if (this.props.showMarker) {
      if (this.props.customMarker) {
        return this.props.customMarker;
      } else {
        return (
          <View style={styles.rectangleContainer}>
            <View
              style={[
                styles.rectangle,
                this.props.markerStyle ? this.props.markerStyle : null,
              ]}
            />
          </View>
        );
      }
    }
    return null;
  }
  _renderCameraComponent() {
    return (
      <Camera
        androidCameraPermissionOptions={{
          title: this.props.permissionDialogTitle,
          message: this.props.permissionDialogMessage,
          buttonPositive: this.props.buttonPositive,
        }}
        style={[styles.camera, this.props.cameraStyle]}
        onBarCodeRead={this._handleBarCodeRead.bind(this)}
        type={this.props.cameraType}
        flashMode={this.props.flashMode}
        captureAudio={false}
        {...this.props.cameraProps}
      >
        {this._renderCameraMarker()}
      </Camera>
    );
  }
  _renderCamera() {
    const {
      notAuthorizedView,
      pendingAuthorizationView,
      cameraType,
      cameraTimeoutView,
    } = this.props;
    if (!this.state.isCameraActivated) {
      return (
        <TouchableWithoutFeedback onPress={() => this._setCamera(true)}>
          {cameraTimeoutView}
        </TouchableWithoutFeedback>
      );
    }
    const { isAuthorized, isAuthorizationChecked } = this.state;
    if (isAuthorized) {
      if (this.props.cameraTimeout > 0) {
        this.timer && clearTimeout(this.timer);
        this.timer = setTimeout(
          () => this._setCamera(false),
          this.props.cameraTimeout
        );
      }
      if (this.props.fadeIn) {
        return (
          <Animated.View
            style={{
              opacity: this.state.fadeInOpacity,
              backgroundColor: 'transparent',
              height:
                (this.props.cameraStyle && this.props.cameraStyle.height) ||
                styles.camera.height,
            }}
          >
            {this._renderCameraComponent()}
          </Animated.View>
        );
      }
      return this._renderCameraComponent();
    } else if (!isAuthorizationChecked) {
      return pendingAuthorizationView;
    } else {
      return notAuthorizedView;
    }
  }
  reactivate() {
    this._setScanning(false);
  }
  render() {
    return (
      <View style={[styles.mainContainer, this.props.containerStyle]}>
        <View style={[styles.infoView, this.props.topViewStyle]}>
          {this._renderTopContent()}
        </View>
        <View style={this.props.cameraContainerStyle}>{this._renderCamera()}</View>
        <View style={[styles.infoView, this.props.bottomViewStyle]}>
          {this._renderBottomContent()}
        </View>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  mainContainer: {
    flex: 1,
  },
  infoView: {
    flex: 2,
    justifyContent: 'center',
    alignItems: 'center',
    width: Dimensions.get('window').width,
  },
  camera: {
    flex: 0,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'transparent',
    height: Dimensions.get('window').width,
    width: Dimensions.get('window').width,
  },
  rectangleContainer: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'transparent',
  },
  rectangle: {
    height: 250,
    width: 250,
    borderWidth: 2,
    borderColor: '#00FF00',
    backgroundColor: 'transparent',
  },
});