From e8ba7235731c8784448f084b1b76a8745f049c96 Mon Sep 17 00:00:00 2001 From: olewandowski1 Date: Fri, 28 Aug 2026 14:22:42 +0200 Subject: [PATCH 1/2] OBLS-936 Fix duplicate product scan in sorted putaway on DataWedge scans The user-directed putaway list decided whether a validation scan was required from React state written in ScannerInput's onChange handler. A DataWedge scan arrives as an intent broadcast, which calls onChange and onSubmit back to back in the same tick, so the submit handler still read the previous value and treated the scan as a manual entry. The product then had to be scanned again after the destination. Keep the flag in a ref so it updates synchronously and both input paths agree. Only the intent path is affected, so this reproduces on Zebra devices running the DataWedge profile (confirmed on TC52 and TC53) but not on the emulator, on keystroke wedge scanners, or when the code is typed by hand, where the submit lands in a later tick. --- .../SortationPutaway/PutawayTaskListScreen.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/screens/SortationPutaway/PutawayTaskListScreen.tsx b/src/screens/SortationPutaway/PutawayTaskListScreen.tsx index a9f984fa..f5f4fe59 100644 --- a/src/screens/SortationPutaway/PutawayTaskListScreen.tsx +++ b/src/screens/SortationPutaway/PutawayTaskListScreen.tsx @@ -1,6 +1,6 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Alert, ScrollView, Text, TouchableOpacity, View } from 'react-native'; import { Button, Chip, Divider, Paragraph } from 'react-native-paper'; import { useDispatch, useSelector } from 'react-redux'; @@ -96,11 +96,12 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const putawayTasks = useSelector((state: RootState) => state.putawayReducer.putawayTasks) as SortationTask[]; const [searchTerm, setSearchTerm] = useState(''); const [expandedZones, setExpandedZones] = useState<{ [key: string]: boolean }>({}); - const [enteredManually, setEnteredManually] = useState(true); + // A hardware scan fires onChange and onSubmit in the same tick, so state would still read its previous value here. + const enteredManually = useRef(true); const resetFilters = () => { setSearchTerm(''); - setEnteredManually(true); + enteredManually.current = true; }; const navigateToTask = (task: SortationTask, requiresValidationScan: boolean) => { @@ -116,7 +117,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const handleScanChange = (text: string) => { setSearchTerm(text); - setEnteredManually(false); + enteredManually.current = false; }; const handleScan = (code: string) => { @@ -127,7 +128,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const matches = (putawayTasks ?? []).filter((task) => isProductBarcodeValid(trimmed, task.inventoryItem?.product)); if (matches.length === 1) { - navigateToTask(matches[0], enteredManually); + navigateToTask(matches[0], enteredManually.current); return; } if (matches.length > 1) { @@ -140,7 +141,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const handleSearchSelect = (productCode: string) => { const matches = (putawayTasks ?? []).filter((task) => task.inventoryItem?.product?.productCode === productCode); - setEnteredManually(true); + enteredManually.current = true; if (matches.length === 1) { navigateToTask(matches[0], true); } else { @@ -218,7 +219,7 @@ export default function PutawayTaskListScreen({ route }: PutawayTaskListScreenPr const selectedTask = zoneTasks.tasks[taskIndex]; - navigateToTask(selectedTask, enteredManually); + navigateToTask(selectedTask, enteredManually.current); }; const handleClearSearch = () => { From 767932a8b9cadcecbae63bef4b62dae18bbf98be Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 19:32:42 +0000 Subject: [PATCH 2/2] OBLS-936 Fix DataWedge profile configuration for Zebra integrated scanner DataWedge rejected every profile config the app sent, logging "Key PROFILE_ENABLED expected String but value was a java.lang.Boolean" and the same for RESET_CONFIG, so the OPENBOXES profile was never enabled and the integrated scanner did not deliver scans to the app. react-native-datawedge-intents builds the config bundle by calling String.valueOf() on the object it is given and re-parsing the result as JSON. A nested object arrives at the native side as a Java Map, whose toString() ("{PROFILE_ENABLED=true, ...}") re-parses with unquoted values as booleans and numbers - DataWedge requires Strings there and drops them. Serialising the config with JSON.stringify keeps every value a String; the bridge parses the JSON into the same nested bundle. Also associate the profile with the package the app is actually running under (DeviceInfo.getBundleId()) instead of a hardcoded com.openboxes.android, so branded and experimental builds whose applicationId carries a suffix activate the profile too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HePztn8cebd1ecp7pmZrPv --- src/hooks/constant.ts | 23 ++++++++++++++++------- src/hooks/useScanListener.ts | 26 +++++++++++++++++++------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/hooks/constant.ts b/src/hooks/constant.ts index 44299bb2..ab13ea59 100644 --- a/src/hooks/constant.ts +++ b/src/hooks/constant.ts @@ -34,8 +34,15 @@ export const PROFILE = { SET_CONFIG_PROFILE: 'com.symbol.datawedge.api.SET_CONFIG' }; -export const PROFILE_CONFIG = { - PROFILE_NAME: 'OPENBOXES', +// DataWedge reads every value of a profile config bundle as a String, so all of the +// values below are quoted (including the booleans and the intent delivery mode). +// See sendDataWedgeConfig in useScanListener for how they reach DataWedge intact. + +// The profile only becomes active for the app it is associated with, so the package +// name has to be the one the app is actually running under - it is not always +// com.openboxes.android (branded and experimental builds append a suffix). +export const getProfileConfig = (packageName: string) => ({ + PROFILE_NAME: PROFILE.NAME, PROFILE_ENABLED: 'true', CONFIG_MODE: 'UPDATE', PLUGIN_CONFIG: { @@ -45,13 +52,14 @@ export const PROFILE_CONFIG = { }, APP_LIST: [ { - PACKAGE_NAME: 'com.openboxes.android', + PACKAGE_NAME: packageName, ACTIVITY_LIST: ['*'] } ] -}; -export const PROFILE_CONFIG2 = { - PROFILE_NAME: 'OPENBOXES', +}); + +export const INTENT_OUTPUT_CONFIG = { + PROFILE_NAME: PROFILE.NAME, PROFILE_ENABLED: 'true', CONFIG_MODE: 'UPDATE', PLUGIN_CONFIG: { @@ -60,6 +68,7 @@ export const PROFILE_CONFIG2 = { PARAM_LIST: { intent_output_enabled: 'true', intent_action: 'com.openboxes.android.ACTION', + // 2 = broadcast intent, which is what registerBroadcastReceiver listens for. intent_delivery: '2' } } @@ -71,7 +80,7 @@ export const PROFILE_CONFIG2 = { // typed into the focused field as keystrokes), and re-enabled otherwise so plain // inputs elsewhere can still be populated by scanning. export const getKeystrokeOutputConfig = (enabled: boolean) => ({ - PROFILE_NAME: 'OPENBOXES', + PROFILE_NAME: PROFILE.NAME, PROFILE_ENABLED: 'true', CONFIG_MODE: 'UPDATE', PLUGIN_CONFIG: { diff --git a/src/hooks/useScanListener.ts b/src/hooks/useScanListener.ts index 669c5235..c1c93010 100644 --- a/src/hooks/useScanListener.ts +++ b/src/hooks/useScanListener.ts @@ -1,16 +1,17 @@ import { useEffect, useRef } from 'react'; import { DeviceEventEmitter, Platform } from 'react-native'; import DataWedgeIntents from 'react-native-datawedge-intents'; +import DeviceInfo from 'react-native-device-info'; import { ACTION, FILTER_ACTIONS, FILTER_CATEGORY, getKeystrokeOutputConfig, + getProfileConfig, + INTENT_OUTPUT_CONFIG, LISTENER, - PROFILE, - PROFILE_CONFIG, - PROFILE_CONFIG2 + PROFILE } from './constant'; export type ScanResult = { @@ -24,25 +25,36 @@ let profileConfigured = false; // muted while this is > 0 (see below). let activeConsumerCount = 0; -function sendDataWedgeCommand(extraName: string, extraValue: unknown): void { +function sendDataWedgeCommand(extraName: string, extraValue: string): void { DataWedgeIntents.sendBroadcastWithExtras({ action: ACTION.API_ACTION, extras: { [extraName]: extraValue, SEND_RESULT: 'false' } }); } +// A profile config is a nested bundle, and the native bridge builds it by calling +// String.valueOf() on whatever it is given and re-parsing that as JSON. Handing it a +// plain object therefore serialises a Java Map ("{PROFILE_ENABLED=true, ...}"), whose +// unquoted values parse back as booleans and numbers - DataWedge then rejects them +// ("Key PROFILE_ENABLED expected String but value was a java.lang.Boolean"), leaves the +// profile disabled and never sends the scan broadcast. Passing real JSON keeps every +// value the String that DataWedge expects. +function sendDataWedgeConfig(config: Record): void { + sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, JSON.stringify(config)); +} + 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); + sendDataWedgeConfig(getProfileConfig(DeviceInfo.getBundleId())); + sendDataWedgeConfig(INTENT_OUTPUT_CONFIG); } function setKeystrokeOutput(enabled: boolean): void { - sendDataWedgeCommand(PROFILE.SET_CONFIG_PROFILE, getKeystrokeOutputConfig(enabled)); + sendDataWedgeConfig(getKeystrokeOutputConfig(enabled)); } function extractScan(intent: Record): ScanResult | null {