1- import React , { useState , useEffect , useCallback } from 'react' ;
1+ import React , { useState , useEffect , useRef , useCallback } from 'react' ;
22import {
33 Text ,
44 View ,
55 StyleSheet ,
66 Dimensions ,
77 TouchableOpacity ,
8- Alert ,
98 ActivityIndicator ,
109} from 'react-native' ;
1110import { BarCodeScanner } from 'expo-barcode-scanner' ;
@@ -27,17 +26,28 @@ interface SessionStats {
2726 skipped : number ;
2827}
2928
29+ const MAX_CONCURRENT = 4 ;
30+ const RATE_LIMIT_MS = 300 ;
31+ const DEDUP_TTL_MS = 5000 ;
32+
33+ export const parseQRCode = ( data : string ) : string | null => {
34+ const match = data . match ( / ^ c h a i n f o r g e : \/ \/ p a c k a g e \/ ( .+ ) $ / ) ;
35+ return match ?. [ 1 ] ?? null ;
36+ } ;
37+
3038export const BulkScannerScreen : React . FC < Props > = ( { navigation } ) => {
3139 const [ hasPermission , setHasPermission ] = useState < boolean | null > ( null ) ;
32- const [ isProcessing , setIsProcessing ] = useState ( false ) ;
33- const [ lastScanResult , setLastScanResult ] = useState < { status : 'success' | 'error' ; message : string } | null > ( null ) ;
40+ const [ inFlightCount , setInFlightCount ] = useState ( 0 ) ;
41+ const [ lastScanResult , setLastScanResult ] = useState < { status : 'success' | 'error' | 'skipped' ; message : string } | null > ( null ) ;
3442 const [ stats , setStats ] = useState < SessionStats > ( {
3543 scanned : 0 ,
3644 verified : 0 ,
3745 failed : 0 ,
3846 skipped : 0 ,
3947 } ) ;
4048
49+ const seenRef = useRef < Map < string , number > > ( new Map ( ) ) ;
50+ const resultTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
4151 const { colors } = useTheme ( ) ;
4252 const { queueClaimConfirmation, isConnected } = useSync ( ) ;
4353
@@ -50,45 +60,76 @@ export const BulkScannerScreen: React.FC<Props> = ({ navigation }) => {
5060 getBarCodeScannerPermissions ( ) ;
5161 } , [ ] ) ;
5262
53- const handleBarCodeScanned = async ( { type, data } : { type : string ; data : string } ) => {
54- if ( isProcessing ) return ;
55- setIsProcessing ( true ) ;
63+ useEffect ( ( ) => {
64+ return ( ) => {
65+ if ( resultTimerRef . current ) clearTimeout ( resultTimerRef . current ) ;
66+ } ;
67+ } , [ ] ) ;
5668
57- // Check if it's the correct format: chainforge://package/{id}
58- const regex = / ^ c h a i n f o r g e : \/ \/ p a c k a g e \/ ( .+ ) $ / ;
59- const match = data . match ( regex ) ;
69+ const showResult = useCallback (
70+ ( result : { status : 'success' | 'error' | 'skipped' ; message : string } ) => {
71+ if ( resultTimerRef . current ) clearTimeout ( resultTimerRef . current ) ;
72+ setLastScanResult ( result ) ;
73+ resultTimerRef . current = setTimeout ( ( ) => setLastScanResult ( null ) , 2000 ) ;
74+ } ,
75+ [ ] ,
76+ ) ;
77+
78+ const handleBarCodeScanned = useCallback (
79+ async ( { type, data } : { type : string ; data : string } ) => {
80+ if ( inFlightCount >= MAX_CONCURRENT ) return ;
6081
61- setStats ( prev => ( { ... prev , scanned : prev . scanned + 1 } ) ) ;
82+ const aidId = parseQRCode ( data ) ;
6283
63- if ( match && match [ 1 ] ) {
64- const aidId = match [ 1 ] ;
65- const claimId = `claim-${ aidId } ` ; // Assuming standard claimId format for bulk verify
84+ if ( ! aidId ) {
85+ setStats ( prev => ( { ...prev , scanned : prev . scanned + 1 , failed : prev . failed + 1 } ) ) ;
86+ showResult ( { status : 'error' , message : 'Invalid ChainForge QR code.' } ) ;
87+ return ;
88+ }
89+
90+ const now = Date . now ( ) ;
91+ const lastSeen = seenRef . current . get ( aidId ) ;
92+
93+ if ( lastSeen && now - lastSeen < DEDUP_TTL_MS ) {
94+ setStats ( prev => ( { ...prev , scanned : prev . scanned + 1 , skipped : prev . skipped + 1 } ) ) ;
95+ showResult ( { status : 'skipped' , message : 'Duplicate scan — skipped.' } ) ;
96+ return ;
97+ }
98+
99+ if ( lastSeen && now - lastSeen < RATE_LIMIT_MS ) {
100+ setStats ( prev => ( { ...prev , scanned : prev . scanned + 1 , skipped : prev . skipped + 1 } ) ) ;
101+ showResult ( { status : 'skipped' , message : 'Rate limited — slow down.' } ) ;
102+ return ;
103+ }
104+
105+ seenRef . current . set ( aidId , now ) ;
106+ setStats ( prev => ( { ...prev , scanned : prev . scanned + 1 } ) ) ;
107+ setInFlightCount ( prev => prev + 1 ) ;
108+
109+ const claimId = `claim-${ aidId } ` ;
66110
67111 try {
68112 const result = await queueClaimConfirmation ( aidId , claimId ) ;
69-
113+
70114 if ( result . status === 'completed' || result . status === 'queued' ) {
71115 setStats ( prev => ( { ...prev , verified : prev . verified + 1 } ) ) ;
72- setLastScanResult ( {
73- status : 'success' ,
74- message : result . status === 'completed' ? 'Package verified successfully!' : 'Package queued for verification (offline).'
116+ showResult ( {
117+ status : 'success' ,
118+ message :
119+ result . status === 'completed'
120+ ? 'Package verified successfully!'
121+ : 'Package queued for verification (offline).' ,
75122 } ) ;
76123 }
77- } catch ( error ) {
124+ } catch {
78125 setStats ( prev => ( { ...prev , failed : prev . failed + 1 } ) ) ;
79- setLastScanResult ( { status : 'error' , message : 'Verification failed. Please try again.' } ) ;
126+ showResult ( { status : 'error' , message : 'Verification failed. Please try again.' } ) ;
127+ } finally {
128+ setInFlightCount ( prev => prev - 1 ) ;
80129 }
81- } else {
82- setStats ( prev => ( { ...prev , failed : prev . failed + 1 } ) ) ;
83- setLastScanResult ( { status : 'error' , message : 'Invalid ChainForge QR code.' } ) ;
84- }
85-
86- // Short delay before allowing the next scan to provide feedback
87- setTimeout ( ( ) => {
88- setIsProcessing ( false ) ;
89- setLastScanResult ( null ) ;
90- } , 2000 ) ;
91- } ;
130+ } ,
131+ [ inFlightCount , queueClaimConfirmation , showResult ] ,
132+ ) ;
92133
93134 if ( hasPermission === null ) {
94135 return (
@@ -112,10 +153,12 @@ export const BulkScannerScreen: React.FC<Props> = ({ navigation }) => {
112153 ) ;
113154 }
114155
156+ const isAtCapacity = inFlightCount >= MAX_CONCURRENT ;
157+
115158 return (
116159 < View style = { styles . container } >
117160 < BarCodeScanner
118- onBarCodeScanned = { isProcessing ? undefined : handleBarCodeScanned }
161+ onBarCodeScanned = { isAtCapacity ? undefined : handleBarCodeScanned }
119162 style = { StyleSheet . absoluteFillObject }
120163 />
121164
@@ -134,31 +177,42 @@ export const BulkScannerScreen: React.FC<Props> = ({ navigation }) => {
134177 < Text style = { [ styles . statValue , { color : colors . error } ] } > { stats . failed } </ Text >
135178 < Text style = { styles . statLabel } > Failed</ Text >
136179 </ View >
180+ < View style = { styles . statItem } >
181+ < Text style = { [ styles . statValue , { color : colors . warning ?? '#FFD700' } ] } > { inFlightCount } </ Text >
182+ < Text style = { styles . statLabel } > In Flight</ Text >
183+ </ View >
137184 </ View >
138185
139186 < View style = { styles . viewfinderContainer } >
140- < View style = { [ styles . viewfinder , isProcessing && styles . viewfinderProcessing ] } />
187+ < View style = { [ styles . viewfinder , isAtCapacity && styles . viewfinderProcessing ] } />
141188 </ View >
142189
143190 { /* Feedback Area */ }
144191 < View style = { styles . feedbackContainer } >
145- { isProcessing && ! lastScanResult && (
192+ { isAtCapacity && ! lastScanResult && (
146193 < View style = { styles . processingIndicator } >
147194 < ActivityIndicator color = "white" size = "small" />
148- < Text style = { styles . processingText } > Processing... </ Text >
195+ < Text style = { styles . processingText } > Processing { inFlightCount } / { MAX_CONCURRENT } … </ Text >
149196 </ View >
150197 ) }
151198
152199 { lastScanResult && (
153200 < View style = { [
154- styles . resultBadge ,
155- { backgroundColor : lastScanResult . status === 'success' ? colors . success : colors . error }
201+ styles . resultBadge ,
202+ {
203+ backgroundColor :
204+ lastScanResult . status === 'success'
205+ ? colors . success
206+ : lastScanResult . status === 'skipped'
207+ ? ( colors . warning ?? '#FFD700' )
208+ : colors . error ,
209+ } ,
156210 ] } >
157211 < Text style = { styles . resultText } > { lastScanResult . message } </ Text >
158212 </ View >
159213 ) }
160214
161- { ! isProcessing && ! lastScanResult && (
215+ { ! isAtCapacity && ! lastScanResult && (
162216 < Text style = { styles . instructionText } > Align QR code to scan</ Text >
163217 ) }
164218
0 commit comments