diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..efdf569 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: 🐛 Report a bug +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' +--- + +### Current behaviour + + +### Expected behaviour + + +### How to reproduce? + + + +### Preview + + +### What have you tried so far? + + +### Your Environment + +| software | version | +| ------------------------------------- | ------- | +| ios | x | +| android | x | +| react-native | x.x.x | +| @pushpendersingh/react-native-scanner | x.x.x | +| node | x.x.x | +| npm or yarn | x.x.x | diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f7a79fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: 🗣 Feature request +about: Suggest an idea for this project +title: '' +labels: 'feature request' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..588bc22 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,11 @@ +--- +name: 💬 Question +about: You need help with @pushpendersingh/react-native-scanner. +title: '' +labels: 'question' +assignees: '' + +--- + +### Ask your Question + diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml deleted file mode 100644 index f918c91..0000000 --- a/.github/actions/setup/action.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Setup -description: Setup Node.js and install dependencies - -runs: - using: composite - steps: - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version-file: .nvmrc - - - name: Cache dependencies - id: yarn-cache - uses: actions/cache@v3 - with: - path: | - **/node_modules - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} - restore-keys: | - ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - ${{ runner.os }}-yarn- - - - name: Install dependencies - if: steps.yarn-cache.outputs.cache-hit != 'true' - run: | - yarn install --cwd example --frozen-lockfile - yarn install --frozen-lockfile - shell: bash diff --git a/.github/workflows/check-repro.yml b/.github/workflows/check-repro.yml new file mode 100644 index 0000000..f0dc2b6 --- /dev/null +++ b/.github/workflows/check-repro.yml @@ -0,0 +1,69 @@ +name: Check for repro +on: + issues: + types: [opened, edited] + issue_comment: + types: [created, edited] + +jobs: + check-repro: + if: ${{ github.event.label.name == 'bug' }} + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const user = context.payload.sender.login; + const body = context.payload.comment + ? context.payload.comment.body + : context.payload.issue.body; + const regex = new RegExp( + `https?:\\/\\/((github\\.com\\/${user}\\/[^/]+\\/?[\\s\\n]+)|(snack\\.expo\\.dev\\/.+))`, + 'gm' + ); + if (regex.test(body)) { + await github.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['repro provided'], + }); + try { + await github.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'needs repro', + }); + } catch (error) { + if (!/Label does not exist/.test(error.message)) { + throw error; + } + } + } else { + if (context.eventName !== 'issues') { + return; + } + const body = "Hey! Thanks for opening the issue. The issue doesn't seem to contain a link to a repro (a [snack.expo.dev](https://snack.expo.dev) link or link to a GitHub repo under your username).\n\nCan you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? A repro will help us debug the issue faster. Please try to keep the repro as small as possible and make sure that we can run it without additional setup."; + const comments = await github.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + }); + if (comments.data.some(comment => comment.body === body)) { + return; + } + await github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + await github.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['needs repro'], + }); + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 39efde8..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: CI -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Lint files - run: yarn lint - - - name: Typecheck files - run: yarn typecheck - - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Run unit tests - run: yarn test --maxWorkers=2 --coverage - - build-library: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Build package - run: yarn prepack - - build-android: - runs-on: ubuntu-latest - env: - TURBO_CACHE_DIR: .turbo/android - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Cache turborepo for Android - uses: actions/cache@v3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-android-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-android- - - - name: Check turborepo cache for Android - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn --silent turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Install JDK - if: env.turbo_cache_hit != 1 - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '11' - - - name: Finalize Android SDK - if: env.turbo_cache_hit != 1 - run: | - /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" - - - name: Cache Gradle - if: env.turbo_cache_hit != 1 - uses: actions/cache@v3 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Build example for Android - run: | - yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" - - build-ios: - runs-on: macos-latest - env: - TURBO_CACHE_DIR: .turbo/ios - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Cache turborepo for iOS - uses: actions/cache@v3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-ios- - - - name: Check turborepo cache for iOS - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn --silent turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Cache cocoapods - if: env.turbo_cache_hit != 1 - id: cocoapods-cache - uses: actions/cache@v3 - with: - path: | - **/ios/Pods - key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-cocoapods- - - - name: Install cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' - run: | - yarn example pods - env: - NO_FLIPPER: 1 - - - name: Build example for iOS - run: | - yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml new file mode 100644 index 0000000..46cf7f1 --- /dev/null +++ b/.github/workflows/semantic-pr.yml @@ -0,0 +1,13 @@ +name: "Semantic Pull Request" +on: [pull_request] + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4.5.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..33ebabc --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,22 @@ + +name: Close stale issues and PRs +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 30 + days-before-close: 7 + any-of-labels: 'needs more info,needs repro,needs response' + exempt-issue-labels: 'repro provided,keep open' + exempt-pr-labels: 'keep open' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + stale-issue-message: 'Hello 👋, this issue has been open for more than a month without a repro or any activity. If the issue is still present in the latest version, please provide a repro or leave a comment within 7 days to keep it open, otherwise it will be closed automatically. If you found a solution or workaround for the issue, please comment here for others to find. If this issue is critical for you, please consider sending a pull request to fix it.' + stale-pr-message: 'Hello 👋, this pull request has been open for more than a month with no activity on it. If you think this is still necessary with the latest version, please comment and ping a maintainer to get this reviewed, otherwise it will be closed automatically in 7 days.' diff --git a/.github/workflows/triage.yaml b/.github/workflows/triage.yaml new file mode 100644 index 0000000..43e1b42 --- /dev/null +++ b/.github/workflows/triage.yaml @@ -0,0 +1,27 @@ +name: Triage +on: + issues: + types: [labeled] + +jobs: + needs-more-info: + runs-on: ubuntu-latest + if: github.event.label.name == 'needs more info' + steps: + - uses: actions/checkout@master + - uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can, to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to reproduce the issue with minimal code (link to [snack.expo.dev](https://snack.expo.dev)) or a repo on GitHub, and the information about your environment (such as the platform of the device, versions of all the packages etc.)." + + needs-repro: + runs-on: ubuntu-latest + if: github.event.label.name == 'needs repro' + steps: + - uses: actions/checkout@master + - uses: actions/github@v1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: comment "Hey! Thanks for opening the issue. Can you provide a minimal repro which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.dev](https://snack.expo.dev). If it's not possible to repro it on [snack.expo.dev](https://snack.expo.dev), then you can also provide the repro in a GitHub repository." diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml new file mode 100644 index 0000000..009ce77 --- /dev/null +++ b/.github/workflows/versions.yml @@ -0,0 +1,19 @@ +name: Check versions +on: + issues: + types: [opened, edited] + +jobs: + check-versions: + if: ${{ github.event.label.name == 'bug' }} + runs-on: ubuntu-latest + steps: + - uses: react-navigation/check-versions-action@v1.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + required-packages: | + react-native + @pushpendersingh/react-native-scanner + optional-packages: | + npm + yarn diff --git a/README.md b/README.md index fa06eaa..9a8d2e5 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,65 @@ # @pushpendersingh/react-native-scanner - A QR code & Barcode Scanner for React Native Projects. - For React Native developers that need to scan barcodes and QR codes in their apps, this package is a useful resource. It supports React Native's new Fabric Native architecture and was created in Kotlin and Objective-C. - With this package, users can quickly and easily scan barcodes and QR codes with their device's camera. Using this package, several types of codes can be scanned, and it is simple to use. If you want to provide your React Native app the ability to read barcodes and QR codes, you should definitely give this package some thought. +The `@pushpendersingh/react-native-scanner` package also includes a flashlight feature that can be turned on and off. This can be useful when scanning QR codes in low light conditions. + ## Getting started ### Requirements - #### IOS - Open your project's `Info.plist` and add the following lines inside the outermost `` tag: - ```xml NSCameraUsageDescription Your message to user when the camera is accessed for the first time ``` + Open your project's `Podfile` and add enable the new architecture: + ``` :fabric_enabled => true, ``` + Run below command to enable the new architecture in IOS folder + ``` bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install ``` - ### Android - Open your project's `AndroidManifest.xml` and add the following lines inside the `` tag: - ```xml ``` + Open your project's `gradle.properties` and add enable the new architecture: + ``` newArchEnabled=true ``` - ### To install and start using @pushpendersingh/react-native-scanner - ```sh npm install @pushpendersingh/react-native-scanner ``` - ### Supported Formats - -| 1D product | 1D industrial | 2D | -|:----------------------|:--------------|:---------------| -| UPC-A | Code 39 | QR Code | -| UPC-E | Code 93 | Data Matrix | -| EAN-8 | Code 128 | Aztec | -| EAN-13 | Codabar | PDF 417 | -| | ITF | | - +| 1D product | 1D industrial | 2D | +| :--------- | :------------ | :---------- | +| UPC-A | Code 39 | QR Code | +| UPC-E | Code 93 | Data Matrix | +| EAN-8 | Code 128 | Aztec | +| EAN-13 | Codabar | PDF 417 | +| | ITF | | ## Usage To use @pushpendersingh/react-native-scanner, `import` the `@pushpendersingh/react-native-scanner` module and use the `` tag. More usage examples can be seen under the `examples/` folder. +
+ Basic usage + Here is an example of basic usage: ```js @@ -74,19 +71,14 @@ import { Text, SafeAreaView } from 'react-native'; - import { request, PERMISSIONS, openSettings, RESULTS } from 'react-native-permissions'; import { ReactNativeScannerView } from "@pushpendersingh/react-native-scanner"; - export default function App() { - const { height, width } = useWindowDimensions(); const [isCameraPermissionGranted, setIsCameraPermissionGranted] = useState(false); - useEffect(() => { checkCameraPermission(); }, []); - const checkCameraPermission = async () => { request(Platform.OS === 'ios' ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA) .then(async (result: any) => { @@ -108,7 +100,6 @@ export default function App() { } }) }; - if (isCameraPermissionGranted) { return ( @@ -130,19 +121,185 @@ export default function App() { } ``` +
+ +## Flashlight Feature + +
+ Flashlight Feature + + To use the flashlight feature, add the following code to your project: + +```jsx +import React, {useEffect, useRef, useState} from 'react'; +import { + Alert, + Platform, + useWindowDimensions, + Text, + SafeAreaView, + TouchableOpacity, +} from 'react-native'; + +import { + request, + PERMISSIONS, + openSettings, + RESULTS, +} from 'react-native-permissions'; +import { + ReactNativeScannerView, + Commands, +} from '@pushpendersingh/react-native-scanner'; + +export default function App() { + const {height, width} = useWindowDimensions(); + const [isCameraPermissionGranted, setIsCameraPermissionGranted] = + useState(false); + const cameraRef = useRef(null); + + useEffect(() => { + checkCameraPermission(); + + return () => { + if(cameraRef.current) { + Commands.releaseCamera(cameraRef.current); + } + }; + }, []); + + const enableFlashlight = () => { + Commands.enableFlashlight(cameraRef.current); + }; + + const disableFlashlight = () => { + Commands.disableFlashlight(cameraRef.current); + }; + + const checkCameraPermission = async () => { + request( + Platform.OS === 'ios' + ? PERMISSIONS.IOS.CAMERA + : PERMISSIONS.ANDROID.CAMERA, + ).then(async (result: any) => { + switch (result) { + case RESULTS.UNAVAILABLE: + break; + case RESULTS.DENIED: + Alert.alert( + 'Permission Denied', + 'You need to grant camera permission first', + ); + openSettings(); + break; + case RESULTS.GRANTED: + setIsCameraPermissionGranted(true); + break; + case RESULTS.BLOCKED: + Alert.alert( + 'Permission Blocked', + 'You need to grant camera permission first', + ); + openSettings(); + break; + } + }); + }; + + if (isCameraPermissionGranted) { + return ( + + (cameraRef.current = ref)} + style={{height, width}} + onQrScanned={(value: any) => { + console.log(value.nativeEvent); + }} + /> + + + Turn ON + + + + Turn OFF + + + ); + } else { + return ( + + You need to grant camera permission first + + ); + } +} +``` + +
+ ## Props #### `onQrScanned` (required) - propType: `func.isRequired` default: `(e) => (console.log('QR code scanned!', e))` In the event that a QR code or barcode is detected in the camera's view, this specified method will be called. +## Native Commands + +The `@pushpendersingh/react-native-scanner` package also includes a few native commands that can be used to control the camera and flashlight. + +### Commands + +#### `enableFlashlight` + +This command is used to turn on the flashlight. +```js +if(cameraRef.current) { + Commands.enableFlashlight(cameraRef.current); +} +``` + +#### `disableFlashlight` + +This command is used to turn off the flashlight. +```js +if(cameraRef.current) { + Commands.disableFlashlight(cameraRef.current); +} +``` + +#### `releaseCamera` + +This command is used to release the camera. + +```js +if(cameraRef.current) { + Commands.releaseCamera(cameraRef.current); +} +``` + ## Contributing See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. - ## License - MIT diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt index 58b0f36..12162bf 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerView.kt @@ -15,7 +15,6 @@ import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.core.content.ContextCompat -import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.EventDispatcher @@ -26,180 +25,274 @@ import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import androidx.camera.core.CameraControl class ReactNativeScannerView(context: Context) : LinearLayout(context) { - private var preview: PreviewView - private var mCameraProvider: ProcessCameraProvider? = null - private lateinit var cameraExecutor: ExecutorService - private lateinit var options: BarcodeScannerOptions - private lateinit var scanner: BarcodeScanner - private var analysisUseCase: ImageAnalysis = ImageAnalysis.Builder() - .build() + private var preview: PreviewView + private var mSurfacePreview: Preview? = null + private var mCameraProvider: ProcessCameraProvider? = null + private var analysisUseCase: ImageAnalysis = ImageAnalysis.Builder() + .build() + private lateinit var cameraControl: CameraControl - companion object { - private const val REQUEST_CODE_PERMISSIONS = 10 - private val REQUIRED_PERMISSIONS = - mutableListOf( - Manifest.permission.CAMERA - ).toTypedArray() - } + private lateinit var options: BarcodeScannerOptions + private lateinit var scanner: BarcodeScanner - init { - val linearLayoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - layoutParams = linearLayoutParams - orientation = VERTICAL + private var isCameraRunning: Boolean = false + private var pauseAfterCapture: Boolean = false + private var isActive: Boolean = false - preview = PreviewView(context) - preview.layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - addView(preview) + companion object { + private val REQUIRED_PERMISSIONS = + mutableListOf( + Manifest.permission.CAMERA + ).toTypedArray() + } + + init { + val linearLayoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + layoutParams = linearLayoutParams + orientation = VERTICAL + + preview = PreviewView(context) + preview.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + addView(preview) - setupLayoutHack() + setupLayoutHack() + manuallyLayoutChildren() + } + + private fun setupLayoutHack() { + Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { manuallyLayoutChildren() + viewTreeObserver.dispatchOnGlobalLayout() + Choreographer.getInstance().postFrameCallback(this) + } + }) + } + + private fun manuallyLayoutChildren() { + for (i in 0 until childCount) { + val child = getChildAt(i) + child.measure( + MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + child.layout(0, 0, child.measuredWidth, child.measuredHeight) } + } - private fun setupLayoutHack() { - Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { - override fun doFrame(frameTimeNanos: Long) { - manuallyLayoutChildren() - viewTreeObserver.dispatchOnGlobalLayout() - Choreographer.getInstance().postFrameCallback(this) - } - }) + fun setUpCamera() { + if (allPermissionsGranted()) { + startCamera() } - private fun manuallyLayoutChildren() { - for (i in 0 until childCount) { - val child = getChildAt(i) - child.measure( - MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) - ) - child.layout(0, 0, child.measuredWidth, child.measuredHeight) - } + // newSingleThreadExecutor() will let us perform analysis on a single worker thread + val cameraExecutor = Executors.newSingleThreadExecutor() + + options = BarcodeScannerOptions.Builder() + .setBarcodeFormats( + Barcode.FORMAT_QR_CODE, + Barcode.FORMAT_AZTEC, + Barcode.FORMAT_CODE_128, + Barcode.FORMAT_CODE_39, + Barcode.FORMAT_CODE_93, + Barcode.FORMAT_CODABAR, + Barcode.FORMAT_DATA_MATRIX, + Barcode.FORMAT_EAN_13, + Barcode.FORMAT_EAN_8, + Barcode.FORMAT_ITF, + Barcode.FORMAT_PDF417, + Barcode.FORMAT_UPC_A, + Barcode.FORMAT_UPC_E + ) + .build() + scanner = BarcodeScanning.getClient(options) + + analysisUseCase.setAnalyzer( + cameraExecutor + ) { imageProxy -> + processImageProxy(scanner, imageProxy) } + } - fun setUpCamera(reactApplicationContext: ReactApplicationContext) { - if (allPermissionsGranted()) { - startCamera(reactApplicationContext) - } + @SuppressLint("UnsafeOptInUsageError") + private fun processImageProxy( + barcodeScanner: BarcodeScanner, + imageProxy: ImageProxy + ) { + imageProxy.image?.let { image -> + val inputImage = + InputImage.fromMediaImage( + image, + imageProxy.imageInfo.rotationDegrees + ) + + if (!isCameraRunning) { + return; + } + + barcodeScanner.process(inputImage) + .addOnSuccessListener { barcodeList -> + // mCameraProvider?.unbindAll() // this line will stop the camera from scanning after the first scan + + if (barcodeList.isNotEmpty()) { + if (pauseAfterCapture) { + pausePreview() + } - cameraExecutor = Executors.newSingleThreadExecutor() - - options = BarcodeScannerOptions.Builder() - .setBarcodeFormats( - Barcode.FORMAT_QR_CODE, - Barcode.FORMAT_AZTEC, - Barcode.FORMAT_CODE_128, - Barcode.FORMAT_CODE_39, - Barcode.FORMAT_CODE_93, - Barcode.FORMAT_CODABAR, - Barcode.FORMAT_DATA_MATRIX, - Barcode.FORMAT_EAN_13, - Barcode.FORMAT_EAN_8, - Barcode.FORMAT_ITF, - Barcode.FORMAT_PDF417, - Barcode.FORMAT_UPC_A, - Barcode.FORMAT_UPC_E - ) - .build() - scanner = BarcodeScanning.getClient(options) - - analysisUseCase.setAnalyzer( - // newSingleThreadExecutor() will let us perform analysis on a single worker thread - Executors.newSingleThreadExecutor() - ) { imageProxy -> - processImageProxy(scanner, imageProxy) + val reactContext = context as ReactContext + val surfaceId = UIManagerHelper.getSurfaceId(reactContext) + val eventDispatcher: EventDispatcher? = + UIManagerHelper.getEventDispatcherForReactTag( + reactContext, id + ) + + barcodeList.forEach { barcode -> + barcode?.let { code -> + eventDispatcher?.dispatchEvent(code.cornerPoints?.let { cornerPoints -> + code.boundingBox?.let { bounds -> + ReactNativeScannerViewEvent(surfaceId, id, code.rawValue + ?: "", bounds, cornerPoints, code.format) + } + }) + } + } + } + } + .addOnFailureListener { + // This failure will happen if the barcode scanning model + // fails to download from Google Play Services + }.addOnCompleteListener { + // When the image is from CameraX analysis use case, must + // call image.close() on received images when finished + // using them. Otherwise, new images may not be received + // or the camera may stall. + imageProxy.image?.close() + imageProxy.close() } } + } + + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + context, it + ) == PackageManager.PERMISSION_GRANTED + } + + private fun startCamera() { + val reactContext = context as ReactContext + val cameraProviderFuture = ProcessCameraProvider.getInstance(reactContext) + + cameraProviderFuture.addListener({ + // Used to bind the lifecycle of cameras to the lifecycle owner + val cameraProvider = cameraProviderFuture.get() + mCameraProvider = cameraProvider - @SuppressLint("UnsafeOptInUsageError") - private fun processImageProxy( - barcodeScanner: BarcodeScanner, - imageProxy: ImageProxy - ) { - imageProxy.image?.let { image -> - val inputImage = - InputImage.fromMediaImage( - image, - imageProxy.imageInfo.rotationDegrees - ) - - barcodeScanner.process(inputImage) - .addOnSuccessListener { barcodeList -> - val barcode = - barcodeList.getOrNull(0) // `rawValue` is the decoded value of the barcode - - barcode?.rawValue?.let { value -> - // mCameraProvider?.unbindAll() // this line will stop the camera from scanning after the first scan - val reactContext = context as ReactContext - val eventDispatcher: EventDispatcher? = - UIManagerHelper.getEventDispatcherForReactTag( - reactContext, id - ) - eventDispatcher?.dispatchEvent(ReactNativeScannerViewEvent(id, value)) - } - } - .addOnFailureListener { - // This failure will happen if the barcode scanning model - // fails to download from Google Play Services - }.addOnCompleteListener { - // When the image is from CameraX analysis use case, must - // call image.close() on received images when finished - // using them. Otherwise, new images may not be received - // or the camera may stall. - imageProxy.image?.close() - imageProxy.close() - } + // Preview + val surfacePreview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(preview.surfaceProvider) } + mSurfacePreview = surfacePreview + + // Select back camera as a default + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + isCameraRunning = true + + try { + // Unbind use cases before rebinding + cameraProvider.unbindAll() + + // Bind use cases to camera + cameraProvider.bindToLifecycle( + (reactContext.currentActivity as AppCompatActivity), + cameraSelector, + surfacePreview, + analysisUseCase + ) + + val camera = cameraProvider.bindToLifecycle( + (reactApplicationContext.currentActivity as AppCompatActivity), + cameraSelector, + surfacePreview, + analysisUseCase + ) + cameraControl = camera.cameraControl + + } catch (exc: Exception) { + isCameraRunning = false + } + }, ContextCompat.getMainExecutor(context)) + } + + fun enableFlashlight() { + cameraControl.enableTorch(true) } - private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { - ContextCompat.checkSelfPermission( - context, it - ) == PackageManager.PERMISSION_GRANTED + fun disableFlashlight() { + cameraControl.enableTorch(false) } - private fun startCamera(reactApplicationContext: ReactApplicationContext) { - - val cameraProviderFuture = ProcessCameraProvider.getInstance(context) - - cameraProviderFuture.addListener({ - // Used to bind the lifecycle of cameras to the lifecycle owner - val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() - mCameraProvider = cameraProvider - // Preview - val surfacePreview = Preview.Builder() - .build() - .also { - it.setSurfaceProvider(preview.surfaceProvider) - } - - // Select back camera as a default - val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA - - try { - // Unbind use cases before rebinding - cameraProvider.unbindAll() - - // Bind use cases to camera - cameraProvider.bindToLifecycle( - (reactApplicationContext.currentActivity as AppCompatActivity), - cameraSelector, - surfacePreview, - analysisUseCase - ) - - } catch (exc: Exception) { - - } + fun releaseCamera() { + cameraExecutor.shutdown() + mCameraProvider?.unbindAll() + } - }, ContextCompat.getMainExecutor(context)) + private fun stopCamera() { + + } + + fun setPauseAfterCapture(value: Boolean) { + pauseAfterCapture = value + } + + fun setIsActive(value: Boolean) { + isActive = value + } + + fun pausePreview() { + if (isCameraRunning) { + isCameraRunning = false + mCameraProvider?.unbind(analysisUseCase) } -} \ No newline at end of file + } + + fun resumePreview() { + if (!isCameraRunning) { + isCameraRunning = true + + try { + val reactContext = context as ReactContext + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + // Bind use cases to camera + mCameraProvider?.bindToLifecycle( + (reactContext.currentActivity as AppCompatActivity), + cameraSelector, + analysisUseCase + ) + } catch (exc: Exception) { + isCameraRunning = false + } + } + } + + fun startScanning() { + setIsActive(true) + } + + fun stopScanning() { + setIsActive(false) + } +} diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt index 7e7e6c4..a1987cc 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewEvent.kt @@ -1,30 +1,61 @@ package com.pushpendersingh.reactnativescanner +import android.graphics.Point +import android.graphics.Rect import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.Event import com.facebook.react.uimanager.events.RCTModernEventEmitter +import com.google.mlkit.vision.barcode.common.Barcode +class ReactNativeScannerViewEvent( + surfaceId: Int, + viewId: Int, + private val qrValue: String, + private val rect: Rect, + private val origin: Array, + private val type: Int + ) : Event(surfaceId, viewId) { -class ReactNativeScannerViewEvent(viewId: Int, private val qrValue: String): Event(viewId) { + override fun getEventName(): String { + return "onQrScanned" + } - override fun getEventName(): String { - return "topOnQrScanned" - } + override fun dispatchModern(rctEventEmitter: RCTModernEventEmitter) { + super.dispatchModern(rctEventEmitter) // if we don't call this, the react native part won't receive the event but because of this line event call two times + rctEventEmitter.receiveEvent( + -1, + viewTag, eventName, + Arguments.createMap() + ) + } - override fun dispatchModern(rctEventEmitter: RCTModernEventEmitter?) { - super.dispatchModern(rctEventEmitter) // if we don't call this, the react native part won't receive the event but because of this line event call two times - rctEventEmitter?.receiveEvent( - -1, - viewTag, eventName, - Arguments.createMap() - ) - } + override fun getEventData(): WritableMap { + val event: WritableMap = Arguments.createMap() + val bounds = Arguments.createMap() + bounds.putArray("origin", getPoints(origin)) + bounds.putInt("width", rect.width()) + bounds.putInt("height", rect.height()) - override fun getEventData(): WritableMap { - val event: WritableMap = Arguments.createMap() - event.putString("value", qrValue) - return event - } + event.putMap("bounds", bounds) + event.putString("data", qrValue) + if (type == Barcode.FORMAT_QR_CODE) + event.putString("type", "QR_CODE") + else + event.putString("type", "UNKNOWN") -} \ No newline at end of file + return event + } + + private fun getPoints(points: Array): WritableArray { + val origin: WritableArray = Arguments.createArray() + for (point in points) { + val pointData: WritableMap = Arguments.createMap() + pointData.putInt("x", point.x) + pointData.putInt("y", point.y) + origin.pushMap(pointData); + } + return origin + } +} diff --git a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt index 068b217..0095dc4 100644 --- a/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt +++ b/android/src/main/java/com/pushpendersingh/reactnativescanner/ReactNativeScannerViewManager.kt @@ -1,13 +1,15 @@ package com.pushpendersingh.reactnativescanner import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray import com.facebook.react.common.MapBuilder import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewManagerDelegate -import com.facebook.react.viewmanagers.ReactNativeScannerViewManagerInterface +import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.viewmanagers.ReactNativeScannerViewManagerDelegate +import com.facebook.react.viewmanagers.ReactNativeScannerViewManagerInterface @ReactModule(name = ReactNativeScannerViewManager.NAME) class ReactNativeScannerViewManager(private val mCallerContext: ReactApplicationContext) : @@ -19,7 +21,7 @@ class ReactNativeScannerViewManager(private val mCallerContext: ReactApplication mDelegate = ReactNativeScannerViewManagerDelegate(this) } - override fun getDelegate(): ViewManagerDelegate? { + override fun getDelegate(): ViewManagerDelegate { return mDelegate } @@ -27,20 +29,86 @@ class ReactNativeScannerViewManager(private val mCallerContext: ReactApplication return NAME } + override fun enableFlashlight(view: ReactNativeScannerView?) { + view?.enableFlashlight() + } + + override fun disableFlashlight(view: ReactNativeScannerView?) { + view?.disableFlashlight() + } + + override fun releaseCamera(view: ReactNativeScannerView?) { + view?.releaseCamera() + } + override fun createViewInstance(reactContext: ThemedReactContext): ReactNativeScannerView { - val reactnativeScannerView = ReactNativeScannerView(mCallerContext) - reactnativeScannerView.setUpCamera(mCallerContext) - return reactnativeScannerView + val scannerView = ReactNativeScannerView(mCallerContext) + scannerView.setUpCamera() + return scannerView } companion object { const val NAME = "ReactNativeScannerView" + + const val COMMAND_PAUSE_PREVIEW = 1 + const val COMMAND_RESUME_PREVIEW = 2 + const val COMMAND_START_SCANNING = 3 + const val COMMAND_STOP_SCANNING = 4 } override fun getExportedCustomDirectEventTypeConstants(): Map { return MapBuilder.of( - "topOnQrScanned", + "onQrScanned", MapBuilder.of("registrationName", "onQrScanned") ) } + + override fun getCommandsMap(): MutableMap { + val map = mutableMapOf() + map["pausePreview"] = COMMAND_PAUSE_PREVIEW + map["resumePreview"] = COMMAND_RESUME_PREVIEW + map["startScanning"] = COMMAND_START_SCANNING + map["stopScanning"] = COMMAND_STOP_SCANNING + return map + } + + override fun receiveCommand(root: ReactNativeScannerView, commandId: String?, args: ReadableArray?) { + when (commandId) { + "pausePreview" -> root.pausePreview() + "resumePreview" -> root.resumePreview() + "startScanning" -> root.startScanning() + "stopScanning" -> root.stopScanning() + else -> { + println("Unsupported Command") + } + } + + super.receiveCommand(root, commandId, args) + } + + @ReactProp(name = "pauseAfterCapture") + override fun setPauseAfterCapture(view: ReactNativeScannerView?, value: Boolean) { + view?.setPauseAfterCapture(value) + } + + @ReactProp(name = "isActive") + override fun setIsActive(view: ReactNativeScannerView?, value: Boolean) { + view?.setIsActive(value) + } + + override fun pausePreview(view: ReactNativeScannerView?) { + view?.pausePreview() + } + + override fun resumePreview(view: ReactNativeScannerView?) { + view?.resumePreview() + } + + override fun startScanning(view: ReactNativeScannerView?) { + view?.startScanning() + } + + override fun stopScanning(view: ReactNativeScannerView?) { + view?.stopScanning() + } } diff --git a/example/package.json b/example/package.json index 896bb97..318f564 100644 --- a/example/package.json +++ b/example/package.json @@ -10,7 +10,7 @@ "test": "jest" }, "dependencies": { - "@pushpendersingh/react-native-scanner": "^1.1.0-beta.2", + "@pushpendersingh/react-native-scanner": "^1.1.0", "react": "18.2.0", "react-native": "0.72.3", "react-native-permissions": "^3.7.3" diff --git a/ios/ReactNativeScannerView.mm b/ios/ReactNativeScannerView.mm index 034c37e..f0bb995 100644 --- a/ios/ReactNativeScannerView.mm +++ b/ios/ReactNativeScannerView.mm @@ -10,17 +10,34 @@ using namespace facebook::react; @interface ReactNativeScannerView () - @end @implementation ReactNativeScannerView { UIView * _view; - + AVCaptureSession *_session; AVCaptureDevice *_device; AVCaptureDeviceInput *_input; AVCaptureMetadataOutput *_output; AVCaptureVideoPreviewLayer *_prevLayer; + + BOOL pauseAfterCapture; + BOOL isActive; +} + ++ (NSArray *)metadataObjectTypes +{ + return @[AVMetadataObjectTypeUPCECode, + AVMetadataObjectTypeCode39Code, + AVMetadataObjectTypeCode39Mod43Code, + AVMetadataObjectTypeEAN13Code, + AVMetadataObjectTypeEAN8Code, + AVMetadataObjectTypeCode93Code, + AVMetadataObjectTypeCode128Code, + AVMetadataObjectTypePDF417Code, + AVMetadataObjectTypeQRCode, + AVMetadataObjectTypeAztecCode, + AVMetadataObjectTypeDataMatrixCode]; } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -30,78 +47,230 @@ + (ComponentDescriptorProvider)componentDescriptorProvider - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - static const auto defaultProps = std::make_shared(); - _props = defaultProps; + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; + _session = [[AVCaptureSession alloc] init]; + _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + + NSError *error = nil; + _input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error]; + if (_input) { + [_session addInput:_input]; + } else { + NSLog(@"%@", [error localizedDescription]); + } + + _output = [[AVCaptureMetadataOutput alloc] init]; + [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; + [_session addOutput:_output]; + + _output.metadataObjectTypes = [ReactNativeScannerView metadataObjectTypes]; + + _prevLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session]; + _prevLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; + [_view.layer addSublayer:_prevLayer]; - _view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; + // Create a dispatch queue. + dispatch_queue_t sessionQueue = dispatch_queue_create("session queue", DISPATCH_QUEUE_SERIAL); + // Use dispatch_async to call the startRunning method on the sessionQueue. + dispatch_async(sessionQueue, ^{ + [self->_session startRunning]; + }); - _session = [[AVCaptureSession alloc] init]; - _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; - NSError *error = nil; + self.contentView = _view; + } + + return self; +} - _input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:&error]; - if (_input) { - [_session addInput:_input]; - } else { - NSLog(@"%@", [error localizedDescription]); +- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection +{ + if (_eventEmitter == nullptr) { + return; + } + + NSMutableArray *validBarCodes = [[NSMutableArray alloc] init]; + NSArray *barCodeTypes = [ReactNativeScannerView metadataObjectTypes]; + + for (AVMetadataObject *metadata in metadataObjects) { + BOOL isValidCode = NO; + for (NSString *type in barCodeTypes) { + if ([metadata.type isEqualToString:type]) { + isValidCode = YES; + break; + } + } + + if (isValidCode == YES) { + [validBarCodes addObject:metadata]; + } } + + // pauseAfterCapture: + // * Pause AVCaptureSession for further processing, after valid barcodes found, + // * Can be resumed back by calling resumePreview from the owner of the component + if (pauseAfterCapture == YES && validBarCodes.count > 0) { + [self pausePreview]; + } + + for (AVMetadataObject *metadata in validBarCodes) { + AVMetadataMachineReadableCodeObject *barCodeObject = (AVMetadataMachineReadableCodeObject *)[_prevLayer transformedMetadataObjectForMetadataObject:(AVMetadataMachineReadableCodeObject *)metadata]; + CGRect highlightViewRect = barCodeObject.bounds; + NSArray *corners = barCodeObject.corners; + NSString *codeString = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; + + CGPoint topLeft, bottomLeft, bottomRight, topRight = CGPointMake(0, 0); + + if (corners.count >= 0) { + topLeft = [self mapObject: corners[0]]; + } + + if (corners.count >= 1) { + bottomLeft = [self mapObject: corners[1]]; + } + + if (corners.count >= 2) { + bottomRight = [self mapObject: corners[2]]; + } + + if (corners.count >= 3) { + topRight = [self mapObject: corners[3]]; + } + + facebook::react::ReactNativeScannerViewEventEmitter::OnQrScannedBounds bounds = { + .width = highlightViewRect.size.width, + .height = highlightViewRect.size.height, + .origin = { + .topLeft = {.x = topLeft.x, .y = topLeft.y}, + .bottomLeft = {.x = bottomLeft.x, .y = bottomLeft.y}, + .bottomRight = {.x = bottomRight.x, .y = bottomRight.y}, + .topRight = {.x = topRight.x, .y = topRight.y} + } + }; + + std::dynamic_pointer_cast(_eventEmitter)->onQrScanned(facebook::react::ReactNativeScannerViewEventEmitter::OnQrScanned{ + .bounds = bounds, + .type = std::string([metadata.type UTF8String]), + .data = std::string([codeString UTF8String]), + .target = std::int32_t([codeString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) + }); + } +} - _output = [[AVCaptureMetadataOutput alloc] init]; - [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; - [_session addOutput:_output]; +- (void)releaseCamera { - _output.metadataObjectTypes = [_output availableMetadataObjectTypes]; + NSLog(@"%@", @"Release Camera"); - _prevLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session]; + if (_session != nil) { + // Stop the session + [_session stopRunning]; - _prevLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; - [_view.layer addSublayer:_prevLayer]; - [_session startRunning]; + // Release the session, input, output, and preview layer + _session = nil; + _input = nil; + _output = nil; + _prevLayer = nil; - self.contentView = _view; - } + } +} - return self; +- (void)enableFlashlight { + if ([_device hasTorch] && [_device isTorchModeSupported:AVCaptureTorchModeOn]) { + NSError *error = nil; + if ([_device lockForConfiguration:&error]) { + [_device setTorchMode:AVCaptureTorchModeOn]; + [_device unlockForConfiguration]; + } else { + // Handle error + NSLog(@"%@", [error localizedDescription]); + } + } } -- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection -{ - CGRect highlightViewRect = CGRectZero; - AVMetadataMachineReadableCodeObject *barCodeObject; - NSString *detectionString = nil; - NSArray *barCodeTypes = @[AVMetadataObjectTypeUPCECode, AVMetadataObjectTypeCode39Code, AVMetadataObjectTypeCode39Mod43Code, - AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode93Code, AVMetadataObjectTypeCode128Code, - AVMetadataObjectTypePDF417Code, AVMetadataObjectTypeQRCode, AVMetadataObjectTypeAztecCode]; - - for (AVMetadataObject *metadata in metadataObjects) { - for (NSString *type in barCodeTypes) { - if ([metadata.type isEqualToString:type]) { - barCodeObject = (AVMetadataMachineReadableCodeObject *)[_prevLayer transformedMetadataObjectForMetadataObject:(AVMetadataMachineReadableCodeObject *)metadata]; - highlightViewRect = barCodeObject.bounds; - detectionString = [(AVMetadataMachineReadableCodeObject *)metadata stringValue]; - break; - } +- (void)disableFlashlight { + if ([_device hasTorch] && [_device isTorchModeSupported:AVCaptureTorchModeOff]) { + NSError *error = nil; + if ([_device lockForConfiguration:&error]) { + [_device setTorchMode:AVCaptureTorchModeOff]; + [_device unlockForConfiguration]; + } else { + // Handle error + NSLog(@"%@", [error localizedDescription]); + } } - if (detectionString != nil) { - if (_eventEmitter != nullptr) { - std::dynamic_pointer_cast(_eventEmitter)->onQrScanned(facebook::react::ReactNativeScannerViewEventEmitter::OnQrScanned{ - .value = std::string([detectionString UTF8String]) - }); - } +} + +- (CGPoint)mapObject:(NSDictionary *)object { + if (object == nil) { + return CGPointMake(0, 0); + } + + return CGPointMake([[object objectForKey:@"X"] doubleValue], [[object objectForKey:@"Y"] doubleValue]); +} + +- (void)setIsActive:(BOOL)active { + isActive = active; + + // Enable/Disable Preview Layer + if (isActive) { + [self resumePreview]; + } else { + [self pausePreview]; + } + + if (isActive == _session.isRunning) { + return; + } + // Start/Stop session + if (isActive) { + [_session startRunning]; + } else { + [_session stopRunning]; } - } } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { + const auto &oldViewProps = *std::static_pointer_cast(_props); + const auto &newViewProps = *std::static_pointer_cast(props); + + pauseAfterCapture = newViewProps.pauseAfterCapture; + [self setIsActive:newViewProps.isActive]; + [super updateProps:props oldProps:oldProps]; } +- (void)handleCommand:(nonnull const NSString *)commandName args:(nonnull const NSArray *)args { + RCTReactNativeScannerViewHandleCommand(self, commandName, args); +} + - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics{ - [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; - _prevLayer.frame = [_view.layer bounds]; + [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; + _prevLayer.frame = [_view.layer bounds]; +} + +- (void)pausePreview { + if ([[_prevLayer connection] isEnabled]) { + [[_prevLayer connection] setEnabled:NO]; + } +} + +- (void)resumePreview { + if (![[_prevLayer connection] isEnabled]) { + [[_prevLayer connection] setEnabled:YES]; + } +} + +- (void)startScanning { + [self setIsActive:YES]; +} + +- (void)stopScanning { + [self setIsActive:NO]; } @end @@ -110,3 +279,4 @@ - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetric { return ReactNativeScannerView.class; } + diff --git a/ios/ReactNativeScannerViewManager.mm b/ios/ReactNativeScannerViewManager.mm index c76fe90..8d5f603 100644 --- a/ios/ReactNativeScannerViewManager.mm +++ b/ios/ReactNativeScannerViewManager.mm @@ -11,5 +11,7 @@ @implementation ReactNativeScannerViewManager RCT_EXPORT_MODULE(ReactNativeScannerView) RCT_EXPORT_VIEW_PROPERTY(onQrScanned, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(pauseAfterCapture, BOOL) +RCT_EXPORT_VIEW_PROPERTY(isActive, BOOL) @end diff --git a/package.json b/package.json index aea7b98..1d02212 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pushpendersingh/react-native-scanner", - "version": "1.1.0-beta.2", + "version": "1.2.0", "description": "A QR code & Barcode Scanner for React Native Projects.", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -86,7 +86,7 @@ "engines": { "node": ">= 16.0.0" }, - "packageManager": "^yarn@1.22.15", + "packageManager": "^yarn@1.22.22", "jest": { "preset": "react-native", "modulePathIgnorePatterns": [ diff --git a/src/ReactNativeScannerViewNativeComponent.ts b/src/ReactNativeScannerViewNativeComponent.ts index e94d37f..f20791c 100644 --- a/src/ReactNativeScannerViewNativeComponent.ts +++ b/src/ReactNativeScannerViewNativeComponent.ts @@ -1,13 +1,52 @@ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; -import type { ViewProps } from 'react-native'; -import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes'; +import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; +import type { HostComponent, ViewProps } from 'react-native'; +import type { DirectEventHandler, Int32, Double } from 'react-native/Libraries/Types/CodegenTypes'; type Event = Readonly<{ - value: string; + bounds: Readonly<{ + width: Double, + height: Double, + origin: Readonly<{ + topLeft: Readonly<{ x: Double, y: Double }>; + bottomLeft: Readonly<{ x: Double, y: Double }>; + bottomRight: Readonly<{ x: Double, y: Double }>; + topRight: Readonly<{ x: Double, y: Double }>; + }> + }>; + type: string; + data: string; + target: Int32; }>; -interface NativeProps extends ViewProps { +export interface NativeProps extends ViewProps { + pauseAfterCapture?: boolean, + isActive?: boolean, onQrScanned?: DirectEventHandler; // Event name should start with "on" } -export default codegenNativeComponent('ReactNativeScannerView'); +interface NativeCommands { + enableFlashlight: (viewRef: React.ElementRef>) => Promise; + disableFlashlight: (viewRef: React.ElementRef>) => Promise; + releaseCamera: (viewRef: React.ElementRef>) => Promise; + pausePreview: (viewRef: React.ElementRef>) => void; + resumePreview: (viewRef: React.ElementRef>) => void; + startScanning: (viewRef: React.ElementRef>) => void; + stopScanning: (viewRef: React.ElementRef>) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: [ + 'enableFlashlight', + 'disableFlashlight', + 'releaseCamera', + 'pausePreview', + 'resumePreview', + 'startScanning', + 'stopScanning' + ], +}); + +export default codegenNativeComponent( + 'ReactNativeScannerView', {} +) as HostComponent; diff --git a/src/ScannerView.android.tsx b/src/ScannerView.android.tsx new file mode 100644 index 0000000..f95b025 --- /dev/null +++ b/src/ScannerView.android.tsx @@ -0,0 +1,3 @@ +import ReactNativeScannerView from './ScannerViewBase' + +export default ReactNativeScannerView; diff --git a/src/ScannerView.ios.tsx b/src/ScannerView.ios.tsx new file mode 100644 index 0000000..f95b025 --- /dev/null +++ b/src/ScannerView.ios.tsx @@ -0,0 +1,3 @@ +import ReactNativeScannerView from './ScannerViewBase' + +export default ReactNativeScannerView; diff --git a/src/ScannerView.tsx b/src/ScannerView.tsx new file mode 100644 index 0000000..0b838f0 --- /dev/null +++ b/src/ScannerView.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Text, View } from 'react-native'; +import type { ScannerViewProps } from "./ScannerViewTypes"; + +const ScannerViewComponent = React.FunctionComponent = () => ( + + + React Native Scanner View does not support this platform. + + +); + +export default ScannerViewComponent; \ No newline at end of file diff --git a/src/ScannerViewBase.tsx b/src/ScannerViewBase.tsx new file mode 100644 index 0000000..079656b --- /dev/null +++ b/src/ScannerViewBase.tsx @@ -0,0 +1,51 @@ +import React, { forwardRef, useCallback, useImperativeHandle, useRef, useEffect } from 'react'; +import type { HostComponent } from 'react-native'; + +import ReactNativeScannerView, { Commands } from './ReactNativeScannerViewNativeComponent'; +import type { NativeProps } from './ReactNativeScannerViewNativeComponent'; +import type { ScannerViewProps, ScannerViewQRScanEvent } from "./ScannerViewTypes"; + +const ScannerViewComponent = forwardRef<{}, ScannerViewProps>(({ + pauseAfterCapture = false, + isActive = false, + onQrScanned: onQrScannedProp, + ...otherProps +}, ref) => { + + const scannerViewRef = useRef> | null>(null); + + useImperativeHandle(ref, () => ({ + pausePreview: () => scannerViewRef.current && Commands.pausePreview(scannerViewRef.current), + resumePreview: () => scannerViewRef.current && Commands.resumePreview(scannerViewRef.current), + startScanning: () => scannerViewRef.current && Commands.startScanning(scannerViewRef.current), + stopScanning: () => scannerViewRef.current && Commands.stopScanning(scannerViewRef.current), + }), [scannerViewRef]) + + useEffect(() => { + const localRef = scannerViewRef.current; + return () => { + // Somehow when the Scanner Component unmounts, the Native Module is not deallocated and keeps on running in the background state + // So manually stopping the camera to avoid this situation. + if (localRef) { + Commands.stopScanning(localRef); + } + }; + }, []); + + const onQrScanned = useCallback((event: ScannerViewQRScanEvent) => { + onQrScannedProp?.(event); + }, [onQrScannedProp]); + + const scannerView = + + return scannerView; +}); + +export default ScannerViewComponent; \ No newline at end of file diff --git a/src/ScannerViewTypes.ts b/src/ScannerViewTypes.ts new file mode 100644 index 0000000..86179e1 --- /dev/null +++ b/src/ScannerViewTypes.ts @@ -0,0 +1,55 @@ +import { + NativeSyntheticEvent, + ViewProps, + UIManagerStatic, +} from 'react-native'; + +type ScannerViewCommands = + | 'pausePreview' + | 'resumePreview' + | 'startScanning' + | 'stopScanning'; + +interface RNScannerViewUIManager extends UIManagerStatic { + getViewManagerConfig: (name: string) => { + Commands: { [key in Commands]: number }; + }; +} + +export type ReactNativeScannerViewUIManager = RNScannerViewUIManager; + +export interface Point { + x: number; + y: number; +} + +export interface Origin { + topLeft: Point; + bottomLeft: Point; + bottomRight: Point; + topRight: Point; +} + +export interface Bounds { + width: number; + height: number; + origin: Origin | Point[]; +} + +export interface ScannerViewNativeEvent { + type: string; + data: string; + target: number; +} + +export interface ScannerViewQRScan extends ScannerViewNativeEvent { + bounds: Bounds; +} + +export type ScannerViewQRScanEvent = NativeSyntheticEvent; + +export interface ScannerViewProps extends ViewProps { + pauseAfterCapture?: boolean; + isActive?: boolean; + onQrScanned?: (event: ScannerViewQRScanEvent) => void; +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 770fdf8..3cc67aa 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,2 +1,4 @@ -export { default as ReactNativeScannerView } from './ReactNativeScannerViewNativeComponent'; -export * from './ReactNativeScannerViewNativeComponent'; +import ReactNativeScannerView from './ScannerView'; + +export { ReactNativeScannerView }; +export default ReactNativeScannerView;