diff --git a/src/components/ScannerInput.tsx b/src/components/ScannerInput.tsx index bd8536d4..34842a30 100644 --- a/src/components/ScannerInput.tsx +++ b/src/components/ScannerInput.tsx @@ -13,6 +13,7 @@ import { useSelector } from 'react-redux'; import { isString } from 'lodash'; import { appConfig } from '../constants'; +import { useScanListener } from '../hooks/useScanListener'; import { RootState } from '../redux/reducers'; import { hideSoftKeyboard, showSoftKeyboard } from '../utils/KeyboardUtils'; import Theme from '../utils/Theme'; @@ -162,6 +163,16 @@ export function ScannerInput({ return () => clearTimeout(timer); }, [value, timeout, onSubmit]); + useScanListener((result) => { + const scanned = result.data; + if (!scanned || scanned === lastSubmittedValue.current) { + return; + } + lastSubmittedValue.current = scanned; + onChange(scanned); + onSubmit(scanned); + }, shouldBeFocused); + const handleFocus = useCallback(() => { if (showKeyboard) { showSoftKeyboard(); @@ -187,7 +198,7 @@ export function ScannerInput({ } const trimmed = value.trim(); - if (trimmed) { + if (trimmed && trimmed !== lastSubmittedValue.current) { lastSubmittedValue.current = trimmed; onSubmit(trimmed); } diff --git a/src/hooks/onBarcodeScanned.ts b/src/hooks/useBarcodeScanned.ts similarity index 74% rename from src/hooks/onBarcodeScanned.ts rename to src/hooks/useBarcodeScanned.ts index d12e913c..0b37e26e 100644 --- a/src/hooks/onBarcodeScanned.ts +++ b/src/hooks/useBarcodeScanned.ts @@ -1,22 +1,16 @@ -import useEventListener from './useEventListener'; +import { useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useEffect, useState } from 'react'; -import { searchBarcode } from '../redux/actions/products'; + import showPopup from '../components/Popup'; +import { searchBarcode } from '../redux/actions/products'; +import { useScanListener } from './useScanListener'; -const onBarcodeScanned = () => { +const useBarcodeScanned = () => { const [state, setState] = useState({ error: null, searchProductCode: null }); const dispatch = useDispatch(); - const barcodeData = useEventListener(); - - useEffect(() => { - if (barcodeData && Object.keys(barcodeData).length !== 0) { - onBarCodeScan(barcodeData.data); - } - }, [barcodeData]); const onBarCodeScan = (query: string) => { onEmptyQuery(query); @@ -25,19 +19,21 @@ const onBarcodeScanned = () => { onError(data, query, () => { dispatch(searchBarcode(query, actionCallback)); }); - } else { - if (data.length == 0) { - onEmptyData(query); - } else { - if (data && Object.keys(data).length !== 0) { - onSuccess(data, query); - } - } + } else if (data.length === 0) { + onEmptyData(query); + } else if (data && Object.keys(data).length !== 0) { + onSuccess(data, query); } }; dispatch(searchBarcode(query, actionCallback)); }; + useScanListener((result) => { + if (result.data) { + onBarCodeScan(result.data); + } + }); + const onError = (data: any, query: any, callback: (data: any) => void) => { showPopup({ title: data.errorMessage ?? `Failed to load search results with value = "${query}"`, @@ -56,7 +52,6 @@ const onBarcodeScanned = () => { message: 'Search query is empty', positiveButton: { text: 'Ok' } }); - return; } }; @@ -85,4 +80,4 @@ const onBarcodeScanned = () => { return state.searchProductCode; }; -export default onBarcodeScanned; +export default useBarcodeScanned; diff --git a/src/hooks/useEventListener.ts b/src/hooks/useEventListener.ts deleted file mode 100644 index 64e9e5b8..00000000 --- a/src/hooks/useEventListener.ts +++ /dev/null @@ -1,185 +0,0 @@ -import React from 'react'; -import { DeviceEventEmitter } from 'react-native'; -import DataWedgeIntents from 'react-native-datawedge-intents'; -import { - ACTION, - FILTER_ACTIONS, - FILTER_CATEGORY, - LISTENER, - PROFILE, - PROFILE_CONFIG, - PROFILE_CONFIG2, - PROPERTY, - VERSION -} from './constant'; -import { useDispatch } from 'react-redux'; - -const useEventListener = () => { - const [event, setEvent] = React.useState(''); - const dispatch = useDispatch(); - const [state, setState] = React.useState({ - ean8checked: true, - ean13checked: true, - code39checked: true, - code128checked: true, - lastApiVisible: false, - lastApiText: 'Messages from DataWedge will go here', - checkBoxesDisabled: true, - scanButtonVisible: false, - dwVersionText: 'Pre 6.3. Please create and configure profile manually. See the ReadMe for more details', - activeProfileText: 'Requires DataWedge 6.3+', - enumeratedScannersText: 'Requires DataWedge 6.3+', - scans: [], - data: {}, - sendCommandResult: 'false' - }); - const broadcastReceiver = (intent: any) => { - // Broadcast received - if (intent.hasOwnProperty(PROPERTY.RESULT_INFO)) { - var commandResult = - intent.RESULT + - ' (' + - intent.COMMAND.substring(intent.COMMAND.lastIndexOf('.') + 1, intent.COMMAND.length) + - ')'; // + JSON.stringify(intent.RESULT_INFO); - commandReceived(commandResult.toLowerCase()); - } - - if (intent.hasOwnProperty(PROPERTY.VERSION_INFO)) { - // The version has been returned (DW 6.3 or higher). Includes the DW version along with other subsystem versions e.g MX - var versionInfo = intent[PROPERTY.VERSION_INFO]; - var datawedgeVersion = versionInfo[PROPERTY.DATAWEDGE]; - // Fire events sequentially so the application can gracefully degrade the functionality available on earlier DW versions - if (datawedgeVersion >= VERSION.V06_3) { - datawedge63(); - } - if (datawedgeVersion >= VERSION.V06_4) { - datawedge64(); - } - if (datawedgeVersion >= VERSION.V06_5) { - datawedge65(); - } - } else if (intent.hasOwnProperty(PROPERTY.RESULT_ENUMERATED_SCANNER)) { - // Return from our request to enumerate the available scanners - var enumeratedScannersObj = intent[PROPERTY.RESULT_ENUMERATED_SCANNER]; - enumerateScanners(enumeratedScannersObj); - } else if (intent.hasOwnProperty(PROPERTY.ACTIVE_PROFILE)) { - // Return from our request to obtain the active profile - var activeProfileObj = intent[PROPERTY.ACTIVE_PROFILE]; - activeProfile(activeProfileObj); - } else if (!intent.hasOwnProperty(PROPERTY.RESULT_INFO)) { - // A barcode has been scanned - barcodeScanned(intent, new Date().toLocaleString()); - } - }; - - React.useEffect(() => { - const callback = (intent: any) => { - setEvent(intent); - broadcastReceiver(intent); - }; - - DeviceEventEmitter.addListener(LISTENER.BROADCAST_INTENT, callback); - DeviceEventEmitter.addListener(LISTENER.BARCODE_SCAN, callback); - DeviceEventEmitter.addListener(LISTENER.ENUMERATED_SCANNER, callback); - registerBroadcastReceiver(); - return () => { - DeviceEventEmitter.removeAllListeners(); - }; - }, [broadcastReceiver, event]); - - const registerBroadcastReceiver = () => { - DataWedgeIntents.registerBroadcastReceiver({ - filterActions: FILTER_ACTIONS, - filterCategories: FILTER_CATEGORY - }); - }; - - const datawedge63 = () => { - // Create a profile for our application - sendCommand(PROFILE.CREATE_PROFILE, PROFILE.NAME); - - state.dwVersionText = '6.3. Please configure profile manually. See ReadMe for more details.'; - - // Although we created the profile we can only configure it with DW 6.4. - sendCommand(PROFILE.ACTIVE_PROFILE, ''); - - // Enumerate the available scanners on the device - sendCommand(PROFILE.ENUMERATE_PROFILE, ''); - - // Functionality of the scan button is available - state.scanButtonVisible = true; - }; - - const datawedge64 = () => { - // Documentation states the ability to set a profile config is only available from DW 6.4. - // For our purposes, this includes setting the decoders and configuring the associated app / output params of the profile. - state.dwVersionText = '6.4.'; - //document.getElementById('info_datawedgeVersion').classList.remove("attention"); - // Decoders are now available - state.checkBoxesDisabled = false; - // Configure the created profile (associated app and keyboard plugin) - sendCommand(PROFILE.SET_CONFIG_PROFILE, PROFILE_CONFIG); - // Configure the created profile (intent plugin) - sendCommand(PROFILE.SET_CONFIG_PROFILE, PROFILE_CONFIG2); - // Give some time for the profile to settle then query its value - setTimeout(() => { - sendCommand(PROFILE.ACTIVE_PROFILE, ''); - }, 1000); - }; - - const sendCommand = (extraName: string, extraValue: any) => { - var broadcastExtras: any = {}; - broadcastExtras[extraName] = extraValue; - broadcastExtras.SEND_RESULT = state.sendCommandResult; - DataWedgeIntents.sendBroadcastWithExtras({ - action: ACTION.API_ACTION, - extras: broadcastExtras - }); - }; - - const datawedge65 = () => { - state.dwVersionText = '6.5 or higher.'; - // Instruct the API to send - state.sendCommandResult = 'true'; - state.lastApiVisible = true; - setState({ ...state }); - }; - - const commandReceived = (commandText: string) => { - state.lastApiText = commandText; - setState({ ...state }); - }; - - const enumerateScanners = (enumeratedScanners: any) => { - var humanReadableScannerList = ''; - for (var i = 0; i < enumeratedScanners.length; i++) { - humanReadableScannerList += enumeratedScanners[i].SCANNER_NAME; - if (i < enumeratedScanners.length - 1) { - humanReadableScannerList += ', '; - } - } - state.enumeratedScannersText = humanReadableScannerList; - }; - - const activeProfile = (theActiveProfile: string) => { - state.activeProfileText = theActiveProfile; - setState({ ...state }); - }; - - const barcodeScanned = (scanData: any, timeOfScan: string) => { - var scannedData = scanData[ACTION.DATA_STRING] ?? scanData.data; - var scannedType = scanData[ACTION.LABEL_TYPE] ?? scanData.labelType; - if (scannedData && scannedType) { - let dataScanned = { - data: scannedData, - decoder: scannedType, - timeAtDecode: timeOfScan - }; - state.data = dataScanned; - state.scans.unshift(dataScanned); - setState({ ...state }); - } - }; - return state.data; -}; -export default useEventListener; diff --git a/src/hooks/useScanListener.ts b/src/hooks/useScanListener.ts new file mode 100644 index 00000000..8068365d --- /dev/null +++ b/src/hooks/useScanListener.ts @@ -0,0 +1,73 @@ +import { useEffect, useRef } from 'react'; +import { DeviceEventEmitter, Platform } from 'react-native'; +import DataWedgeIntents from 'react-native-datawedge-intents'; + +import { + ACTION, + FILTER_ACTIONS, + FILTER_CATEGORY, + LISTENER, + PROFILE, + PROFILE_CONFIG, + PROFILE_CONFIG2 +} from './constant'; + +export type ScanResult = { + data: string; + labelType?: string; +}; + +let profileConfigured = false; + +function sendDataWedgeCommand(extraName: string, extraValue: unknown): void { + DataWedgeIntents.sendBroadcastWithExtras({ + action: ACTION.API_ACTION, + extras: { [extraName]: extraValue, SEND_RESULT: 'false' } + }); +} + +function configureProfileOnce(): void { + if (profileConfigured) { + return; + } + profileConfigured = true; + sendDataWedgeCommand(PROFILE.CREATE_PROFILE, PROFILE.NAME); + sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, PROFILE_CONFIG); + sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, PROFILE_CONFIG2); +} + +function extractScan(intent: Record): ScanResult | null { + const data = intent?.[ACTION.DATA_STRING] ?? intent?.data; + if (!data) { + return null; + } + const labelType = intent?.[ACTION.LABEL_TYPE] ?? intent?.labelType; + return { data: String(data).trim(), labelType: labelType ? String(labelType) : undefined }; +} + +export function useScanListener(onScan: (result: ScanResult) => void, enabled = true): void { + const onScanRef = useRef(onScan); + onScanRef.current = onScan; + + useEffect(() => { + if (!enabled || Platform.OS !== 'android' || !DataWedgeIntents?.registerBroadcastReceiver) { + return; + } + + configureProfileOnce(); + DataWedgeIntents.registerBroadcastReceiver({ + filterActions: FILTER_ACTIONS, + filterCategories: FILTER_CATEGORY + }); + + const handleIntent = (intent: Record) => { + const result = extractScan(intent); + if (result) { + onScanRef.current(result); + } + }; + + const subscription = DeviceEventEmitter.addListener(LISTENER.BROADCAST_INTENT, handleIntent); + return () => subscription.remove(); + }, [enabled]); +} diff --git a/src/screens/Scan/index.tsx b/src/screens/Scan/index.tsx index f4a14cb3..07d73138 100644 --- a/src/screens/Scan/index.tsx +++ b/src/screens/Scan/index.tsx @@ -4,7 +4,7 @@ import { View } from 'react-native'; import styles from './styles'; import { useNavigation } from '@react-navigation/native'; import Product from '../../data/product/Product'; -import onBarcodeScanned from '../../hooks/onBarcodeScanned'; +import useBarcodeScanned from '../../hooks/useBarcodeScanned'; import EmptyView from '../../components/EmptyView'; import BarcodeSearchHeader from '../../components/BarcodeSearchHeader/BarcodeSearchHeader'; import { searchBarcode } from '../../redux/actions/products'; @@ -12,7 +12,7 @@ import showPopup from '../../components/Popup'; import { useDispatch } from 'react-redux'; const Scan = () => { - const barcodeData = onBarcodeScanned(); + const barcodeData = useBarcodeScanned(); const navigation = useNavigation(); const dispatch = useDispatch();