@mleonard9/vin-scanner 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/ios/VisionCameraBarcodeScanner.m +2 -23
- package/ios/VisionCameraTextRecognition.m +2 -21
- package/lib/commonjs/ManualVinInput.js +25 -15
- package/lib/commonjs/ManualVinInput.js.map +1 -1
- package/lib/commonjs/PendingVinBanner.js +44 -21
- package/lib/commonjs/PendingVinBanner.js.map +1 -1
- package/lib/commonjs/ScannerChromeOverlay.js +185 -0
- package/lib/commonjs/ScannerChromeOverlay.js.map +1 -0
- package/lib/commonjs/TextVinPrompt.js +33 -18
- package/lib/commonjs/TextVinPrompt.js.map +1 -1
- package/lib/commonjs/index.js +11 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/scanBarcodes.js +25 -24
- package/lib/commonjs/scanBarcodes.js.map +1 -1
- package/lib/commonjs/scanText.js +22 -22
- package/lib/commonjs/scanText.js.map +1 -1
- package/lib/commonjs/useVinScanner.js +15 -4
- package/lib/commonjs/useVinScanner.js.map +1 -1
- package/lib/commonjs/vinUtils.js +6 -4
- package/lib/commonjs/vinUtils.js.map +1 -1
- package/lib/module/ManualVinInput.js +25 -15
- package/lib/module/ManualVinInput.js.map +1 -1
- package/lib/module/PendingVinBanner.js +45 -22
- package/lib/module/PendingVinBanner.js.map +1 -1
- package/lib/module/ScannerChromeOverlay.js +177 -0
- package/lib/module/ScannerChromeOverlay.js.map +1 -0
- package/lib/module/TextVinPrompt.js +33 -18
- package/lib/module/TextVinPrompt.js.map +1 -1
- package/lib/module/index.js +5 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/scanBarcodes.js +25 -24
- package/lib/module/scanBarcodes.js.map +1 -1
- package/lib/module/scanText.js +22 -22
- package/lib/module/scanText.js.map +1 -1
- package/lib/module/useVinScanner.js +15 -4
- package/lib/module/useVinScanner.js.map +1 -1
- package/lib/module/vinUtils.js +6 -4
- package/lib/module/vinUtils.js.map +1 -1
- package/lib/typescript/src/ManualVinInput.d.ts.map +1 -1
- package/lib/typescript/src/PendingVinBanner.d.ts +2 -1
- package/lib/typescript/src/PendingVinBanner.d.ts.map +1 -1
- package/lib/typescript/src/ScannerChromeOverlay.d.ts +19 -0
- package/lib/typescript/src/ScannerChromeOverlay.d.ts.map +1 -0
- package/lib/typescript/src/TextVinPrompt.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/scanBarcodes.d.ts.map +1 -1
- package/lib/typescript/src/scanText.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +8 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useVinScanner.d.ts.map +1 -1
- package/lib/typescript/src/vinUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ManualVinInput.tsx +22 -15
- package/src/PendingVinBanner.tsx +66 -27
- package/src/ScannerChromeOverlay.tsx +214 -0
- package/src/TextVinPrompt.tsx +31 -16
- package/src/index.tsx +6 -2
- package/src/scanBarcodes.ts +46 -40
- package/src/scanText.ts +34 -45
- package/src/types.ts +8 -2
- package/src/useVinScanner.ts +20 -4
- package/src/vinUtils.ts +5 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Pressable,
|
|
5
|
+
Text,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
type ScannerChromeOverlayProps = {
|
|
12
|
+
onBackPress?: () => void;
|
|
13
|
+
onManualEntryPress?: () => void;
|
|
14
|
+
onFlashPress?: () => void;
|
|
15
|
+
isTorchOn?: boolean;
|
|
16
|
+
backContent?: React.ReactNode;
|
|
17
|
+
manualEntryContent?: React.ReactNode;
|
|
18
|
+
flashOnContent?: React.ReactNode;
|
|
19
|
+
flashOffContent?: React.ReactNode;
|
|
20
|
+
centerContent?: React.ReactNode;
|
|
21
|
+
frameSize?: number;
|
|
22
|
+
cornerColor?: string;
|
|
23
|
+
topOffset?: number;
|
|
24
|
+
bottomOffset?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ActionButton = ({
|
|
28
|
+
onPress,
|
|
29
|
+
children,
|
|
30
|
+
style,
|
|
31
|
+
}: {
|
|
32
|
+
onPress?: () => void;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
style?: StyleProp<ViewStyle>;
|
|
35
|
+
}) => {
|
|
36
|
+
if (!onPress) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Pressable onPress={onPress} style={[styles.actionButton, style]}>
|
|
42
|
+
{children}
|
|
43
|
+
</Pressable>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function ScannerChromeOverlay({
|
|
48
|
+
onBackPress,
|
|
49
|
+
onManualEntryPress,
|
|
50
|
+
onFlashPress,
|
|
51
|
+
isTorchOn = false,
|
|
52
|
+
backContent,
|
|
53
|
+
manualEntryContent,
|
|
54
|
+
flashOnContent,
|
|
55
|
+
flashOffContent,
|
|
56
|
+
centerContent,
|
|
57
|
+
frameSize = 240,
|
|
58
|
+
cornerColor = 'white',
|
|
59
|
+
topOffset = 64,
|
|
60
|
+
bottomOffset = 64,
|
|
61
|
+
}: ScannerChromeOverlayProps) {
|
|
62
|
+
const halfFrame = frameSize / 2;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<View pointerEvents="box-none" style={styles.actionContainer}>
|
|
67
|
+
<ActionButton
|
|
68
|
+
onPress={onBackPress}
|
|
69
|
+
style={[styles.backButton, { top: topOffset }]}
|
|
70
|
+
>
|
|
71
|
+
{backContent ?? <Text style={styles.actionText}>Back</Text>}
|
|
72
|
+
</ActionButton>
|
|
73
|
+
|
|
74
|
+
<ActionButton
|
|
75
|
+
onPress={onManualEntryPress}
|
|
76
|
+
style={[styles.manualButton, { top: topOffset }]}
|
|
77
|
+
>
|
|
78
|
+
{manualEntryContent ?? <Text style={styles.actionText}>VIN</Text>}
|
|
79
|
+
</ActionButton>
|
|
80
|
+
|
|
81
|
+
<ActionButton
|
|
82
|
+
onPress={onFlashPress}
|
|
83
|
+
style={[styles.flashButton, { bottom: bottomOffset }]}
|
|
84
|
+
>
|
|
85
|
+
{isTorchOn
|
|
86
|
+
? (flashOnContent ?? (
|
|
87
|
+
<Text style={styles.actionText}>Torch Off</Text>
|
|
88
|
+
))
|
|
89
|
+
: (flashOffContent ?? <Text style={styles.actionText}>Torch</Text>)}
|
|
90
|
+
</ActionButton>
|
|
91
|
+
|
|
92
|
+
{centerContent ? (
|
|
93
|
+
<View style={styles.centerContent}>{centerContent}</View>
|
|
94
|
+
) : null}
|
|
95
|
+
</View>
|
|
96
|
+
|
|
97
|
+
<View
|
|
98
|
+
pointerEvents="none"
|
|
99
|
+
style={[
|
|
100
|
+
styles.viewFinderOverlay,
|
|
101
|
+
{
|
|
102
|
+
width: frameSize,
|
|
103
|
+
height: frameSize,
|
|
104
|
+
marginLeft: -halfFrame,
|
|
105
|
+
marginTop: -halfFrame,
|
|
106
|
+
},
|
|
107
|
+
]}
|
|
108
|
+
>
|
|
109
|
+
<Corner position="topLeft" color={cornerColor} />
|
|
110
|
+
<Corner position="topRight" color={cornerColor} />
|
|
111
|
+
<Corner position="bottomLeft" color={cornerColor} />
|
|
112
|
+
<Corner position="bottomRight" color={cornerColor} />
|
|
113
|
+
</View>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Corner({
|
|
119
|
+
position,
|
|
120
|
+
color,
|
|
121
|
+
}: {
|
|
122
|
+
position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
|
123
|
+
color: string;
|
|
124
|
+
}) {
|
|
125
|
+
const styleMap = {
|
|
126
|
+
topLeft: styles.cornerTopLeft,
|
|
127
|
+
topRight: styles.cornerTopRight,
|
|
128
|
+
bottomLeft: styles.cornerBottomLeft,
|
|
129
|
+
bottomRight: styles.cornerBottomRight,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return <View style={[styleMap[position], { borderColor: color }]} />;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const styles = StyleSheet.create({
|
|
136
|
+
actionContainer: {
|
|
137
|
+
...StyleSheet.absoluteFillObject,
|
|
138
|
+
zIndex: 100,
|
|
139
|
+
},
|
|
140
|
+
actionButton: {
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
zIndex: 101,
|
|
143
|
+
paddingHorizontal: 12,
|
|
144
|
+
paddingVertical: 8,
|
|
145
|
+
borderRadius: 999,
|
|
146
|
+
backgroundColor: 'rgba(0, 0, 0, 0.45)',
|
|
147
|
+
},
|
|
148
|
+
backButton: {
|
|
149
|
+
left: 24,
|
|
150
|
+
},
|
|
151
|
+
manualButton: {
|
|
152
|
+
right: 24,
|
|
153
|
+
},
|
|
154
|
+
flashButton: {
|
|
155
|
+
left: 24,
|
|
156
|
+
},
|
|
157
|
+
actionText: {
|
|
158
|
+
color: 'white',
|
|
159
|
+
fontSize: 14,
|
|
160
|
+
fontWeight: '700',
|
|
161
|
+
},
|
|
162
|
+
centerContent: {
|
|
163
|
+
position: 'absolute',
|
|
164
|
+
top: 40,
|
|
165
|
+
left: 0,
|
|
166
|
+
right: 0,
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
zIndex: 101,
|
|
169
|
+
},
|
|
170
|
+
viewFinderOverlay: {
|
|
171
|
+
position: 'absolute',
|
|
172
|
+
top: '50%',
|
|
173
|
+
left: '50%',
|
|
174
|
+
borderColor: 'transparent',
|
|
175
|
+
},
|
|
176
|
+
cornerTopLeft: {
|
|
177
|
+
position: 'absolute',
|
|
178
|
+
top: 0,
|
|
179
|
+
left: 0,
|
|
180
|
+
width: 20,
|
|
181
|
+
height: 20,
|
|
182
|
+
borderTopWidth: 2,
|
|
183
|
+
borderLeftWidth: 2,
|
|
184
|
+
},
|
|
185
|
+
cornerTopRight: {
|
|
186
|
+
position: 'absolute',
|
|
187
|
+
top: 0,
|
|
188
|
+
right: 0,
|
|
189
|
+
width: 20,
|
|
190
|
+
height: 20,
|
|
191
|
+
borderTopWidth: 2,
|
|
192
|
+
borderRightWidth: 2,
|
|
193
|
+
},
|
|
194
|
+
cornerBottomLeft: {
|
|
195
|
+
position: 'absolute',
|
|
196
|
+
bottom: 0,
|
|
197
|
+
left: 0,
|
|
198
|
+
width: 20,
|
|
199
|
+
height: 20,
|
|
200
|
+
borderBottomWidth: 2,
|
|
201
|
+
borderLeftWidth: 2,
|
|
202
|
+
},
|
|
203
|
+
cornerBottomRight: {
|
|
204
|
+
position: 'absolute',
|
|
205
|
+
bottom: 0,
|
|
206
|
+
right: 0,
|
|
207
|
+
width: 20,
|
|
208
|
+
height: 20,
|
|
209
|
+
borderBottomWidth: 2,
|
|
210
|
+
borderRightWidth: 2,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export default ScannerChromeOverlay;
|
package/src/TextVinPrompt.tsx
CHANGED
|
@@ -32,7 +32,7 @@ export function TextVinPrompt({
|
|
|
32
32
|
onDismiss,
|
|
33
33
|
title = 'VIN detected',
|
|
34
34
|
subtitle,
|
|
35
|
-
buttonLabel = '
|
|
35
|
+
buttonLabel = 'Book It',
|
|
36
36
|
buttonColor = '#0A84FF',
|
|
37
37
|
}: TextVinPromptProps) {
|
|
38
38
|
if (!visible) return null;
|
|
@@ -52,8 +52,11 @@ export function TextVinPrompt({
|
|
|
52
52
|
style={styles.list}
|
|
53
53
|
contentContainerStyle={styles.listContent}
|
|
54
54
|
>
|
|
55
|
-
{candidates.map((candidate) => (
|
|
56
|
-
<View
|
|
55
|
+
{candidates.map((candidate, index) => (
|
|
56
|
+
<View
|
|
57
|
+
key={`${candidate.value}-${index}`}
|
|
58
|
+
style={[styles.row, index > 0 && styles.rowBorder]}
|
|
59
|
+
>
|
|
57
60
|
<Text style={styles.vin} numberOfLines={1}>
|
|
58
61
|
{candidate.value}
|
|
59
62
|
</Text>
|
|
@@ -81,7 +84,7 @@ export function TextVinPrompt({
|
|
|
81
84
|
const styles = StyleSheet.create({
|
|
82
85
|
backdrop: {
|
|
83
86
|
flex: 1,
|
|
84
|
-
backgroundColor: 'rgba(0,0,0,0.
|
|
87
|
+
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
85
88
|
alignItems: 'center',
|
|
86
89
|
justifyContent: 'center',
|
|
87
90
|
padding: 16,
|
|
@@ -89,38 +92,50 @@ const styles = StyleSheet.create({
|
|
|
89
92
|
sheet: {
|
|
90
93
|
width: '100%',
|
|
91
94
|
maxWidth: 420,
|
|
92
|
-
backgroundColor: '
|
|
93
|
-
borderRadius:
|
|
94
|
-
|
|
95
|
+
backgroundColor: 'white',
|
|
96
|
+
borderRadius: 20,
|
|
97
|
+
paddingVertical: 24,
|
|
98
|
+
paddingHorizontal: 16,
|
|
99
|
+
shadowColor: '#000',
|
|
100
|
+
shadowOffset: { width: 0, height: 2 },
|
|
101
|
+
shadowOpacity: 0.25,
|
|
102
|
+
shadowRadius: 4,
|
|
103
|
+
elevation: 5,
|
|
95
104
|
},
|
|
96
105
|
title: {
|
|
97
|
-
color: '
|
|
106
|
+
color: '#111827',
|
|
98
107
|
fontSize: 18,
|
|
99
108
|
fontWeight: '700',
|
|
100
|
-
marginBottom:
|
|
109
|
+
marginBottom: 8,
|
|
101
110
|
},
|
|
102
111
|
subtitle: {
|
|
103
|
-
color: '#
|
|
112
|
+
color: '#4B5563',
|
|
104
113
|
fontSize: 14,
|
|
105
|
-
marginBottom:
|
|
114
|
+
marginBottom: 12,
|
|
106
115
|
},
|
|
107
116
|
list: {
|
|
108
117
|
maxHeight: 220,
|
|
109
118
|
},
|
|
110
119
|
listContent: {
|
|
111
|
-
|
|
120
|
+
paddingBottom: 4,
|
|
112
121
|
},
|
|
113
122
|
row: {
|
|
114
123
|
flexDirection: 'row',
|
|
115
124
|
alignItems: 'center',
|
|
116
125
|
justifyContent: 'space-between',
|
|
117
|
-
|
|
126
|
+
paddingVertical: 10,
|
|
127
|
+
},
|
|
128
|
+
rowBorder: {
|
|
129
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
130
|
+
borderTopColor: '#E5E7EB',
|
|
118
131
|
},
|
|
119
132
|
vin: {
|
|
120
133
|
flex: 1,
|
|
121
|
-
color: '
|
|
134
|
+
color: '#111827',
|
|
122
135
|
fontSize: 16,
|
|
136
|
+
fontWeight: '700',
|
|
123
137
|
letterSpacing: 1,
|
|
138
|
+
marginRight: 12,
|
|
124
139
|
},
|
|
125
140
|
button: {
|
|
126
141
|
paddingHorizontal: 14,
|
|
@@ -133,13 +148,13 @@ const styles = StyleSheet.create({
|
|
|
133
148
|
fontWeight: '700',
|
|
134
149
|
},
|
|
135
150
|
dismiss: {
|
|
136
|
-
marginTop:
|
|
151
|
+
marginTop: 8,
|
|
137
152
|
alignSelf: 'flex-end',
|
|
138
153
|
paddingHorizontal: 8,
|
|
139
154
|
paddingVertical: 6,
|
|
140
155
|
},
|
|
141
156
|
dismissText: {
|
|
142
|
-
color: '#
|
|
157
|
+
color: '#6B7280',
|
|
143
158
|
fontSize: 14,
|
|
144
159
|
},
|
|
145
160
|
});
|
package/src/index.tsx
CHANGED
|
@@ -45,7 +45,9 @@ export const Camera = forwardRef(function Camera(
|
|
|
45
45
|
|
|
46
46
|
const handleDetections = useRunOnJS(
|
|
47
47
|
(payload: WorkletPayload) => {
|
|
48
|
-
const candidates = buildVinCandidates(payload, resolvedOptions)
|
|
48
|
+
const candidates = buildVinCandidates(payload, resolvedOptions).filter(
|
|
49
|
+
(c) => c.confidence >= resolvedOptions.detection.minConfidence
|
|
50
|
+
);
|
|
49
51
|
const barcodeCandidates = candidates.filter(
|
|
50
52
|
(c) => c.source === 'barcode'
|
|
51
53
|
);
|
|
@@ -306,8 +308,9 @@ function useTextScanner(
|
|
|
306
308
|
}
|
|
307
309
|
return createTextRecognitionPlugin({
|
|
308
310
|
language: options.language,
|
|
311
|
+
validationPattern: options.validationPattern,
|
|
309
312
|
});
|
|
310
|
-
}, [options.enabled, options.language]);
|
|
313
|
+
}, [options.enabled, options.language, options.validationPattern]);
|
|
311
314
|
}
|
|
312
315
|
|
|
313
316
|
export type {
|
|
@@ -334,3 +337,4 @@ export { isValidVin } from './vinUtils';
|
|
|
334
337
|
export { TextVinPrompt } from './TextVinPrompt';
|
|
335
338
|
export { PendingVinBanner } from './PendingVinBanner';
|
|
336
339
|
export { ManualVinInput } from './ManualVinInput';
|
|
340
|
+
export { ScannerChromeOverlay } from './ScannerChromeOverlay';
|
package/src/scanBarcodes.ts
CHANGED
|
@@ -40,15 +40,6 @@ const KNOWN_FORMATS: BarcodeFormat[] = [
|
|
|
40
40
|
];
|
|
41
41
|
|
|
42
42
|
type NativeBarcodeOptionMap = Partial<Record<BarcodeFormat, boolean>>;
|
|
43
|
-
type SerializableValue =
|
|
44
|
-
| string
|
|
45
|
-
| number
|
|
46
|
-
| boolean
|
|
47
|
-
| null
|
|
48
|
-
| undefined
|
|
49
|
-
| ArrayBuffer
|
|
50
|
-
| SerializableValue[]
|
|
51
|
-
| { [key: string]: SerializableValue };
|
|
52
43
|
|
|
53
44
|
const toNativeBarcodeOptions = (
|
|
54
45
|
formats?: ScanBarcodeOptions
|
|
@@ -73,36 +64,47 @@ const unwrapPluginFrame = (frame: Frame): Frame => {
|
|
|
73
64
|
return candidate.__frame ?? frame;
|
|
74
65
|
};
|
|
75
66
|
|
|
76
|
-
const
|
|
67
|
+
const toSerializableArgs = (args: BarcodePluginArgs): BarcodePluginArgs => {
|
|
77
68
|
'worklet';
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
const serialized: BarcodePluginArgs = {};
|
|
70
|
+
const formats: BarcodeFormat[] = [
|
|
71
|
+
'all',
|
|
72
|
+
'aztec',
|
|
73
|
+
'code-128',
|
|
74
|
+
'code-39',
|
|
75
|
+
'code-93',
|
|
76
|
+
'codabar',
|
|
77
|
+
'data-matrix',
|
|
78
|
+
'ean-13',
|
|
79
|
+
'ean-8',
|
|
80
|
+
'itf',
|
|
81
|
+
'pdf-417',
|
|
82
|
+
'qr',
|
|
83
|
+
'upc-a',
|
|
84
|
+
'upc-e',
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
formats.forEach((format) => {
|
|
88
|
+
const value = args[format];
|
|
89
|
+
if (typeof value === 'boolean') {
|
|
90
|
+
serialized[format] = value;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (args.orientation) {
|
|
95
|
+
serialized.orientation = args.orientation;
|
|
91
96
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
|
|
98
|
+
if (args.scanRegion) {
|
|
99
|
+
serialized.scanRegion = {
|
|
100
|
+
x: args.scanRegion.x,
|
|
101
|
+
y: args.scanRegion.y,
|
|
102
|
+
width: args.scanRegion.width,
|
|
103
|
+
height: args.scanRegion.height,
|
|
104
|
+
};
|
|
99
105
|
}
|
|
100
|
-
return undefined;
|
|
101
|
-
};
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
'worklet';
|
|
105
|
-
return toSerializableValue(args) as BarcodePluginArgs;
|
|
107
|
+
return serialized;
|
|
106
108
|
};
|
|
107
109
|
|
|
108
110
|
const toFloat32Array = (input: unknown): Float32Array | undefined => {
|
|
@@ -214,11 +216,15 @@ export function createBarcodeScannerPlugin(
|
|
|
214
216
|
overrides || orientation
|
|
215
217
|
? mergeArgs(baseOptions, overrides, orientation ?? null)
|
|
216
218
|
: baseOptions;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
try {
|
|
220
|
+
const result = plugin.call(
|
|
221
|
+
unwrapPluginFrame(frame),
|
|
222
|
+
toSerializableArgs(args)
|
|
223
|
+
);
|
|
224
|
+
return normalizeBarcodeDetections(result);
|
|
225
|
+
} catch {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
222
228
|
},
|
|
223
229
|
};
|
|
224
230
|
}
|
package/src/scanText.ts
CHANGED
|
@@ -12,16 +12,6 @@ import type {
|
|
|
12
12
|
TextRecognitionPlugin,
|
|
13
13
|
} from './types';
|
|
14
14
|
|
|
15
|
-
type SerializableValue =
|
|
16
|
-
| string
|
|
17
|
-
| number
|
|
18
|
-
| boolean
|
|
19
|
-
| null
|
|
20
|
-
| undefined
|
|
21
|
-
| ArrayBuffer
|
|
22
|
-
| SerializableValue[]
|
|
23
|
-
| { [key: string]: SerializableValue };
|
|
24
|
-
|
|
25
15
|
const LINKING_ERROR: string =
|
|
26
16
|
`The package '@mleonard9/vin-scanner' doesn't seem to be linked. Make sure: \n\n` +
|
|
27
17
|
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
|
|
@@ -36,42 +26,37 @@ const unwrapPluginFrame = (frame: Frame): Frame => {
|
|
|
36
26
|
return candidate.__frame ?? frame;
|
|
37
27
|
};
|
|
38
28
|
|
|
39
|
-
const toSerializableValue = (value: unknown): SerializableValue => {
|
|
40
|
-
'worklet';
|
|
41
|
-
if (
|
|
42
|
-
value == null ||
|
|
43
|
-
typeof value === 'string' ||
|
|
44
|
-
typeof value === 'number' ||
|
|
45
|
-
typeof value === 'boolean'
|
|
46
|
-
) {
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
if (value instanceof ArrayBuffer) {
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
if (Array.isArray(value)) {
|
|
53
|
-
return value.map((item) => toSerializableValue(item));
|
|
54
|
-
}
|
|
55
|
-
if (typeof value === 'object') {
|
|
56
|
-
const record = value as Record<string, unknown>;
|
|
57
|
-
const output: { [key: string]: SerializableValue } = {};
|
|
58
|
-
Object.keys(record).forEach((key) => {
|
|
59
|
-
output[key] = toSerializableValue(record[key]);
|
|
60
|
-
});
|
|
61
|
-
return output;
|
|
62
|
-
}
|
|
63
|
-
return undefined;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
29
|
const toSerializableArgs = (
|
|
67
30
|
args: TextRecognitionOptions & {
|
|
68
31
|
orientation?: FrameOrientation;
|
|
69
32
|
}
|
|
70
|
-
)
|
|
33
|
+
): TextRecognitionOptions & {
|
|
34
|
+
orientation?: FrameOrientation;
|
|
35
|
+
} => {
|
|
71
36
|
'worklet';
|
|
72
|
-
|
|
37
|
+
const serialized: TextRecognitionOptions & {
|
|
73
38
|
orientation?: FrameOrientation;
|
|
74
|
-
};
|
|
39
|
+
} = {};
|
|
40
|
+
|
|
41
|
+
if (args.language) {
|
|
42
|
+
serialized.language = args.language;
|
|
43
|
+
}
|
|
44
|
+
if (args.validationPattern) {
|
|
45
|
+
serialized.validationPattern = args.validationPattern;
|
|
46
|
+
}
|
|
47
|
+
if (args.orientation) {
|
|
48
|
+
serialized.orientation = args.orientation;
|
|
49
|
+
}
|
|
50
|
+
if (args.scanRegion) {
|
|
51
|
+
serialized.scanRegion = {
|
|
52
|
+
x: args.scanRegion.x,
|
|
53
|
+
y: args.scanRegion.y,
|
|
54
|
+
width: args.scanRegion.width,
|
|
55
|
+
height: args.scanRegion.height,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return serialized;
|
|
75
60
|
};
|
|
76
61
|
|
|
77
62
|
const toFloat32Array = (input: unknown): Float32Array | undefined => {
|
|
@@ -194,11 +179,15 @@ export function createTextRecognitionPlugin(
|
|
|
194
179
|
overrides || orientation
|
|
195
180
|
? mergeTextArgs(options, overrides, orientation ?? null)
|
|
196
181
|
: options;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
182
|
+
try {
|
|
183
|
+
const result = plugin.call(
|
|
184
|
+
unwrapPluginFrame(frame),
|
|
185
|
+
toSerializableArgs(args)
|
|
186
|
+
);
|
|
187
|
+
return normalizeTextDetections(result);
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
202
191
|
},
|
|
203
192
|
};
|
|
204
193
|
}
|
package/src/types.ts
CHANGED
|
@@ -152,6 +152,12 @@ export type BarcodeScannerOptions = {
|
|
|
152
152
|
export type TextScannerOptions = {
|
|
153
153
|
enabled?: boolean;
|
|
154
154
|
language?: TextRecognitionLanguage;
|
|
155
|
+
/**
|
|
156
|
+
* Optional regex pattern forwarded to the native text recognizer to reduce
|
|
157
|
+
* bridge traffic before VIN validation runs in JS.
|
|
158
|
+
* Default: `[A-Z0-9]{10,}`
|
|
159
|
+
*/
|
|
160
|
+
validationPattern?: string;
|
|
155
161
|
/**
|
|
156
162
|
* When true, text VINs are surfaced via `onTextPending` and must be
|
|
157
163
|
* confirmed manually; barcode VINs still emit immediately.
|
|
@@ -212,7 +218,7 @@ export type DetectionOptions = {
|
|
|
212
218
|
/**
|
|
213
219
|
* Enable intelligent frame quality checks to skip blurry or dark frames.
|
|
214
220
|
* This improves accuracy by only processing high-quality frames.
|
|
215
|
-
*
|
|
221
|
+
* Set to `false` to disable the luma/sharpness gates entirely.
|
|
216
222
|
*/
|
|
217
223
|
enableFrameQualityCheck?: boolean;
|
|
218
224
|
/**
|
|
@@ -227,7 +233,7 @@ export type DetectionOptions = {
|
|
|
227
233
|
minSharpness?: number;
|
|
228
234
|
/**
|
|
229
235
|
* Minimum candidate confidence required before emitting.
|
|
230
|
-
* Default: 0.
|
|
236
|
+
* Default: 0 (checksum validity is the primary acceptance gate).
|
|
231
237
|
*/
|
|
232
238
|
minConfidence?: number;
|
|
233
239
|
/**
|
package/src/useVinScanner.ts
CHANGED
|
@@ -23,6 +23,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
23
23
|
>([]);
|
|
24
24
|
const pendingTextTimestampRef = useRef<number | null>(null);
|
|
25
25
|
const pendingTextRef = useRef<VinCandidate[]>([]);
|
|
26
|
+
const pendingTextKeyRef = useRef('');
|
|
26
27
|
const pendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
27
28
|
const hapticsEnabled = options?.haptics ?? true;
|
|
28
29
|
const sessionSeen = useRef<Set<string>>(new Set());
|
|
@@ -42,7 +43,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
42
43
|
}
|
|
43
44
|
return createTextRecognitionPlugin({
|
|
44
45
|
language: resolvedOptions.text.language,
|
|
45
|
-
validationPattern:
|
|
46
|
+
validationPattern: resolvedOptions.text.validationPattern,
|
|
46
47
|
});
|
|
47
48
|
}, [resolvedOptions.text]);
|
|
48
49
|
|
|
@@ -57,6 +58,16 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
57
58
|
|
|
58
59
|
const emitTextPending = useRunOnJS(
|
|
59
60
|
(pending: VinCandidate[], ts: number) => {
|
|
61
|
+
const pendingKey = pending
|
|
62
|
+
.map((candidate) => candidate.value)
|
|
63
|
+
.sort()
|
|
64
|
+
.join('|');
|
|
65
|
+
|
|
66
|
+
if (pendingKey.length === 0 || pendingKey === pendingTextKeyRef.current) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pendingTextKeyRef.current = pendingKey;
|
|
60
71
|
pendingTextTimestampRef.current = ts;
|
|
61
72
|
pendingTextRef.current = pending;
|
|
62
73
|
setPendingTextCandidates(pending);
|
|
@@ -66,6 +77,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
66
77
|
const ttl = resolvedOptions.text.pendingTtlMs;
|
|
67
78
|
if (ttl > 0) {
|
|
68
79
|
pendingTimerRef.current = setTimeout(() => {
|
|
80
|
+
pendingTextKeyRef.current = '';
|
|
69
81
|
pendingTextTimestampRef.current = null;
|
|
70
82
|
pendingTextRef.current = [];
|
|
71
83
|
setPendingTextCandidates([]);
|
|
@@ -227,7 +239,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
227
239
|
noBarcodeFrameCount.current = 0;
|
|
228
240
|
useBarcodeFallback.current = false;
|
|
229
241
|
}
|
|
230
|
-
const firstCandidate = pickFirstCandidate(
|
|
242
|
+
const firstCandidate = pickFirstCandidate(filtered, resolvedOptions);
|
|
231
243
|
|
|
232
244
|
const t3 = Date.now();
|
|
233
245
|
|
|
@@ -236,7 +248,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
236
248
|
const event: VinScannerEvent = {
|
|
237
249
|
timestamp: payload.timestamp,
|
|
238
250
|
duration,
|
|
239
|
-
candidates,
|
|
251
|
+
candidates: filtered,
|
|
240
252
|
firstCandidate,
|
|
241
253
|
raw: {
|
|
242
254
|
barcodes: payload.barcodes,
|
|
@@ -300,7 +312,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
300
312
|
lastEmittedVin.current = firstCandidate.value;
|
|
301
313
|
lastEmitTimestamp.current = now;
|
|
302
314
|
sessionSeen.current.add(firstCandidate.value);
|
|
303
|
-
emitResult(
|
|
315
|
+
emitResult(filtered, event);
|
|
304
316
|
}
|
|
305
317
|
}
|
|
306
318
|
},
|
|
@@ -321,6 +333,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
321
333
|
clearTimeout(pendingTimerRef.current);
|
|
322
334
|
pendingTimerRef.current = null;
|
|
323
335
|
}
|
|
336
|
+
pendingTextKeyRef.current = '';
|
|
324
337
|
pendingTextTimestampRef.current = null;
|
|
325
338
|
pendingTextRef.current = [];
|
|
326
339
|
setPendingTextCandidates([]);
|
|
@@ -334,6 +347,9 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
334
347
|
};
|
|
335
348
|
|
|
336
349
|
emitResult(pending, event);
|
|
350
|
+
if (selected?.value) {
|
|
351
|
+
sessionSeen.current.add(selected.value);
|
|
352
|
+
}
|
|
337
353
|
lastEmittedVin.current = selected?.value ?? null;
|
|
338
354
|
lastEmitTimestamp.current = Date.now();
|
|
339
355
|
},
|