@mleonard9/vin-scanner 1.5.1 → 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.
Files changed (64) hide show
  1. package/README.md +19 -2
  2. package/ios/VisionCameraBarcodeScanner.m +2 -23
  3. package/ios/VisionCameraTextRecognition.m +2 -21
  4. package/lib/commonjs/ManualVinInput.js +25 -15
  5. package/lib/commonjs/ManualVinInput.js.map +1 -1
  6. package/lib/commonjs/PendingVinBanner.js +44 -21
  7. package/lib/commonjs/PendingVinBanner.js.map +1 -1
  8. package/lib/commonjs/ScannerChromeOverlay.js +185 -0
  9. package/lib/commonjs/ScannerChromeOverlay.js.map +1 -0
  10. package/lib/commonjs/TextVinPrompt.js +33 -18
  11. package/lib/commonjs/TextVinPrompt.js.map +1 -1
  12. package/lib/commonjs/index.js +11 -3
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/scanBarcodes.js +6 -2
  15. package/lib/commonjs/scanBarcodes.js.map +1 -1
  16. package/lib/commonjs/scanText.js +6 -2
  17. package/lib/commonjs/scanText.js.map +1 -1
  18. package/lib/commonjs/useVinScanner.js +15 -4
  19. package/lib/commonjs/useVinScanner.js.map +1 -1
  20. package/lib/commonjs/vinUtils.js +6 -4
  21. package/lib/commonjs/vinUtils.js.map +1 -1
  22. package/lib/module/ManualVinInput.js +25 -15
  23. package/lib/module/ManualVinInput.js.map +1 -1
  24. package/lib/module/PendingVinBanner.js +45 -22
  25. package/lib/module/PendingVinBanner.js.map +1 -1
  26. package/lib/module/ScannerChromeOverlay.js +177 -0
  27. package/lib/module/ScannerChromeOverlay.js.map +1 -0
  28. package/lib/module/TextVinPrompt.js +33 -18
  29. package/lib/module/TextVinPrompt.js.map +1 -1
  30. package/lib/module/index.js +5 -3
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/scanBarcodes.js +6 -2
  33. package/lib/module/scanBarcodes.js.map +1 -1
  34. package/lib/module/scanText.js +6 -2
  35. package/lib/module/scanText.js.map +1 -1
  36. package/lib/module/useVinScanner.js +15 -4
  37. package/lib/module/useVinScanner.js.map +1 -1
  38. package/lib/module/vinUtils.js +6 -4
  39. package/lib/module/vinUtils.js.map +1 -1
  40. package/lib/typescript/src/ManualVinInput.d.ts.map +1 -1
  41. package/lib/typescript/src/PendingVinBanner.d.ts +2 -1
  42. package/lib/typescript/src/PendingVinBanner.d.ts.map +1 -1
  43. package/lib/typescript/src/ScannerChromeOverlay.d.ts +19 -0
  44. package/lib/typescript/src/ScannerChromeOverlay.d.ts.map +1 -0
  45. package/lib/typescript/src/TextVinPrompt.d.ts.map +1 -1
  46. package/lib/typescript/src/index.d.ts +1 -0
  47. package/lib/typescript/src/index.d.ts.map +1 -1
  48. package/lib/typescript/src/scanBarcodes.d.ts.map +1 -1
  49. package/lib/typescript/src/scanText.d.ts.map +1 -1
  50. package/lib/typescript/src/types.d.ts +8 -2
  51. package/lib/typescript/src/types.d.ts.map +1 -1
  52. package/lib/typescript/src/useVinScanner.d.ts.map +1 -1
  53. package/lib/typescript/src/vinUtils.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/ManualVinInput.tsx +22 -15
  56. package/src/PendingVinBanner.tsx +66 -27
  57. package/src/ScannerChromeOverlay.tsx +214 -0
  58. package/src/TextVinPrompt.tsx +31 -16
  59. package/src/index.tsx +6 -2
  60. package/src/scanBarcodes.ts +9 -5
  61. package/src/scanText.ts +9 -5
  62. package/src/types.ts +8 -2
  63. package/src/useVinScanner.ts +20 -4
  64. package/src/vinUtils.ts +5 -1
@@ -32,7 +32,7 @@ export function TextVinPrompt({
32
32
  onDismiss,
33
33
  title = 'VIN detected',
34
34
  subtitle,
35
- buttonLabel = 'Use VIN',
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 key={candidate.value} style={styles.row}>
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.55)',
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: '#0D0D0D',
93
- borderRadius: 16,
94
- padding: 16,
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: 'white',
106
+ color: '#111827',
98
107
  fontSize: 18,
99
108
  fontWeight: '700',
100
- marginBottom: 12,
109
+ marginBottom: 8,
101
110
  },
102
111
  subtitle: {
103
- color: '#D1D5DB',
112
+ color: '#4B5563',
104
113
  fontSize: 14,
105
- marginBottom: 8,
114
+ marginBottom: 12,
106
115
  },
107
116
  list: {
108
117
  maxHeight: 220,
109
118
  },
110
119
  listContent: {
111
- gap: 12,
120
+ paddingBottom: 4,
112
121
  },
113
122
  row: {
114
123
  flexDirection: 'row',
115
124
  alignItems: 'center',
116
125
  justifyContent: 'space-between',
117
- gap: 12,
126
+ paddingVertical: 10,
127
+ },
128
+ rowBorder: {
129
+ borderTopWidth: StyleSheet.hairlineWidth,
130
+ borderTopColor: '#E5E7EB',
118
131
  },
119
132
  vin: {
120
133
  flex: 1,
121
- color: 'white',
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: 12,
151
+ marginTop: 8,
137
152
  alignSelf: 'flex-end',
138
153
  paddingHorizontal: 8,
139
154
  paddingVertical: 6,
140
155
  },
141
156
  dismissText: {
142
- color: '#9CA3AF',
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';
@@ -216,11 +216,15 @@ export function createBarcodeScannerPlugin(
216
216
  overrides || orientation
217
217
  ? mergeArgs(baseOptions, overrides, orientation ?? null)
218
218
  : baseOptions;
219
- const result = plugin.call(
220
- unwrapPluginFrame(frame),
221
- toSerializableArgs(args)
222
- );
223
- return normalizeBarcodeDetections(result);
219
+ try {
220
+ const result = plugin.call(
221
+ unwrapPluginFrame(frame),
222
+ toSerializableArgs(args)
223
+ );
224
+ return normalizeBarcodeDetections(result);
225
+ } catch {
226
+ return [];
227
+ }
224
228
  },
225
229
  };
226
230
  }
package/src/scanText.ts CHANGED
@@ -179,11 +179,15 @@ export function createTextRecognitionPlugin(
179
179
  overrides || orientation
180
180
  ? mergeTextArgs(options, overrides, orientation ?? null)
181
181
  : options;
182
- const result = plugin.call(
183
- unwrapPluginFrame(frame),
184
- toSerializableArgs(args)
185
- );
186
- return normalizeTextDetections(result);
182
+ try {
183
+ const result = plugin.call(
184
+ unwrapPluginFrame(frame),
185
+ toSerializableArgs(args)
186
+ );
187
+ return normalizeTextDetections(result);
188
+ } catch {
189
+ return [];
190
+ }
187
191
  },
188
192
  };
189
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
- * Note: currently unused; retained for backward compatibility.
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.6
236
+ * Default: 0 (checksum validity is the primary acceptance gate).
231
237
  */
232
238
  minConfidence?: number;
233
239
  /**
@@ -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: '[A-HJ-NPR-Z0-9]{10,}',
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(candidates, resolvedOptions);
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(candidates, event);
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
  },
package/src/vinUtils.ts CHANGED
@@ -66,6 +66,7 @@ const DEFAULT_RESOLVED_OPTIONS: ResolvedVinScannerOptions = {
66
66
  text: {
67
67
  enabled: true,
68
68
  language: 'latin',
69
+ validationPattern: '[A-Z0-9]{10,}',
69
70
  requireConfirmation: false,
70
71
  pendingTtlMs: 5000,
71
72
  },
@@ -76,7 +77,7 @@ const DEFAULT_RESOLVED_OPTIONS: ResolvedVinScannerOptions = {
76
77
  scanRegion: { x: 0.15, y: 0.15, width: 0.7, height: 0.7 },
77
78
  minLuma: 30,
78
79
  minSharpness: 12,
79
- minConfidence: 0.6,
80
+ minConfidence: 0,
80
81
  barcodeFallbackAfter: 45,
81
82
  },
82
83
  showOverlay: false,
@@ -111,6 +112,9 @@ export const resolveOptions = (
111
112
  enabled: options?.text?.enabled ?? DEFAULT_RESOLVED_OPTIONS.text.enabled,
112
113
  language:
113
114
  options?.text?.language ?? DEFAULT_RESOLVED_OPTIONS.text.language,
115
+ validationPattern:
116
+ options?.text?.validationPattern ??
117
+ DEFAULT_RESOLVED_OPTIONS.text.validationPattern,
114
118
  requireConfirmation:
115
119
  options?.text?.requireConfirmation ??
116
120
  DEFAULT_RESOLVED_OPTIONS.text.requireConfirmation,