@mleonard9/vin-scanner 1.4.0 → 1.4.3

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 (48) hide show
  1. package/README.md +6 -38
  2. package/ios/VisionCameraBarcodeScanner.m +4 -4
  3. package/ios/VisionCameraTextRecognition.m +5 -5
  4. package/lib/commonjs/ManualVinInput.js +1 -2
  5. package/lib/commonjs/ManualVinInput.js.map +1 -1
  6. package/lib/commonjs/PendingVinBanner.js.map +1 -1
  7. package/lib/commonjs/TextVinPrompt.js.map +1 -1
  8. package/lib/commonjs/haptics.js +1 -1
  9. package/lib/commonjs/haptics.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/scanBarcodes.js.map +1 -1
  12. package/lib/commonjs/scanText.js.map +1 -1
  13. package/lib/commonjs/useVinScanner.js.map +1 -1
  14. package/lib/commonjs/vinUtils.js +8 -8
  15. package/lib/commonjs/vinUtils.js.map +1 -1
  16. package/lib/module/ManualVinInput.js +1 -2
  17. package/lib/module/ManualVinInput.js.map +1 -1
  18. package/lib/module/PendingVinBanner.js.map +1 -1
  19. package/lib/module/TextVinPrompt.js.map +1 -1
  20. package/lib/module/haptics.js +1 -1
  21. package/lib/module/haptics.js.map +1 -1
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/scanBarcodes.js.map +1 -1
  24. package/lib/module/scanText.js.map +1 -1
  25. package/lib/module/useVinScanner.js.map +1 -1
  26. package/lib/module/vinUtils.js +8 -8
  27. package/lib/module/vinUtils.js.map +1 -1
  28. package/lib/typescript/src/ManualVinInput.d.ts.map +1 -1
  29. package/lib/typescript/src/PendingVinBanner.d.ts.map +1 -1
  30. package/lib/typescript/src/TextVinPrompt.d.ts.map +1 -1
  31. package/lib/typescript/src/haptics.d.ts.map +1 -1
  32. package/lib/typescript/src/index.d.ts.map +1 -1
  33. package/lib/typescript/src/scanBarcodes.d.ts.map +1 -1
  34. package/lib/typescript/src/scanText.d.ts.map +1 -1
  35. package/lib/typescript/src/types.d.ts.map +1 -1
  36. package/lib/typescript/src/useVinScanner.d.ts.map +1 -1
  37. package/lib/typescript/src/vinUtils.d.ts.map +1 -1
  38. package/package.json +1 -3
  39. package/src/ManualVinInput.tsx +44 -5
  40. package/src/PendingVinBanner.tsx +4 -5
  41. package/src/TextVinPrompt.tsx +10 -2
  42. package/src/haptics.ts +2 -5
  43. package/src/index.tsx +46 -22
  44. package/src/scanBarcodes.ts +5 -1
  45. package/src/scanText.ts +5 -1
  46. package/src/types.ts +9 -4
  47. package/src/useVinScanner.ts +38 -28
  48. package/src/vinUtils.ts +112 -70
@@ -10,8 +10,32 @@ type ManualVinInputProps = {
10
10
  placeholder?: string;
11
11
  };
12
12
 
13
- const VIN_CHARS = ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','R','S','T','U','V','W','X','Y','Z'];
14
- const DIGITS = ['0','1','2','3','4','5','6','7','8','9'];
13
+ const VIN_CHARS = [
14
+ 'A',
15
+ 'B',
16
+ 'C',
17
+ 'D',
18
+ 'E',
19
+ 'F',
20
+ 'G',
21
+ 'H',
22
+ 'J',
23
+ 'K',
24
+ 'L',
25
+ 'M',
26
+ 'N',
27
+ 'P',
28
+ 'R',
29
+ 'S',
30
+ 'T',
31
+ 'U',
32
+ 'V',
33
+ 'W',
34
+ 'X',
35
+ 'Y',
36
+ 'Z',
37
+ ];
38
+ const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
15
39
 
16
40
  export function ManualVinInput({
17
41
  initialValue = '',
@@ -43,7 +67,14 @@ export function ManualVinInput({
43
67
  <View style={styles.container}>
44
68
  <TextInput
45
69
  value={value}
46
- onChangeText={(txt) => setValue(txt.replace(/[^A-HJ-NPR-Z0-9]/gi, '').slice(0,17).toUpperCase())}
70
+ onChangeText={(txt) =>
71
+ setValue(
72
+ txt
73
+ .replace(/[^A-HJ-NPR-Z0-9]/gi, '')
74
+ .slice(0, 17)
75
+ .toUpperCase()
76
+ )
77
+ }
47
78
  style={[styles.input, showError && styles.errorInput]}
48
79
  placeholder={placeholder}
49
80
  autoCapitalize="characters"
@@ -61,7 +92,7 @@ export function ManualVinInput({
61
92
  <Key label="⌫" onPress={handleBackspace} wide />
62
93
  </View>
63
94
  <Pressable
64
- style={[styles.submit, { backgroundColor: buttonColor, opacity: isValid ? 1 : 0.5 }]}
95
+ style={[styles.submit, { backgroundColor: buttonColor }]}
65
96
  disabled={!isValid}
66
97
  onPress={handleSubmit}
67
98
  >
@@ -72,7 +103,15 @@ export function ManualVinInput({
72
103
  );
73
104
  }
74
105
 
75
- function Key({ label, onPress, wide }: { label: string; onPress: () => void; wide?: boolean }) {
106
+ function Key({
107
+ label,
108
+ onPress,
109
+ wide,
110
+ }: {
111
+ label: string;
112
+ onPress: () => void;
113
+ wide?: boolean;
114
+ }) {
76
115
  return (
77
116
  <Pressable onPress={onPress} style={[styles.key, wide && styles.keyWide]}>
78
117
  <Text style={styles.keyText}>{label}</Text>
@@ -43,14 +43,13 @@ export function PendingVinBanner({
43
43
  return (
44
44
  <Animated.View
45
45
  pointerEvents={visible ? 'auto' : 'none'}
46
- style={[
47
- styles.container,
48
- { transform: [{ translateY }], opacity: anim },
49
- ]}
46
+ style={[styles.container, { transform: [{ translateY }], opacity: anim }]}
50
47
  >
51
48
  <View style={styles.header}>
52
49
  <Text style={styles.label}>
53
- {candidates.length > 1 ? `${candidates.length} VINs detected` : 'VIN detected'}
50
+ {candidates.length > 1
51
+ ? `${candidates.length} VINs detected`
52
+ : 'VIN detected'}
54
53
  </Text>
55
54
  {onDismiss && (
56
55
  <Pressable onPress={onDismiss} hitSlop={12} style={styles.dismiss}>
@@ -38,12 +38,20 @@ export function TextVinPrompt({
38
38
  if (!visible) return null;
39
39
 
40
40
  return (
41
- <Modal transparent animationType="fade" visible={visible} onRequestClose={onDismiss}>
41
+ <Modal
42
+ transparent
43
+ animationType="fade"
44
+ visible={visible}
45
+ onRequestClose={onDismiss}
46
+ >
42
47
  <View style={styles.backdrop}>
43
48
  <View style={styles.sheet}>
44
49
  <Text style={styles.title}>{title}</Text>
45
50
  {subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
46
- <ScrollView style={styles.list} contentContainerStyle={styles.listContent}>
51
+ <ScrollView
52
+ style={styles.list}
53
+ contentContainerStyle={styles.listContent}
54
+ >
47
55
  {candidates.map((candidate) => (
48
56
  <View key={candidate.value} style={styles.row}>
49
57
  <Text style={styles.vin} numberOfLines={1}>
package/src/haptics.ts CHANGED
@@ -1,16 +1,13 @@
1
1
  let haptic: any = null;
2
2
  try {
3
3
  // Optional dependency: react-native-haptic-feedback
4
- // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
4
+
5
5
  haptic = require('react-native-haptic-feedback');
6
6
  } catch {
7
7
  haptic = null;
8
8
  }
9
9
 
10
- type HapticType =
11
- | 'success'
12
- | 'warning'
13
- | 'impactLight';
10
+ type HapticType = 'success' | 'warning' | 'impactLight';
14
11
 
15
12
  const trigger = (type: HapticType) => {
16
13
  if (!haptic?.default?.trigger) {
package/src/index.tsx CHANGED
@@ -46,7 +46,9 @@ export const Camera = forwardRef(function Camera(
46
46
  const handleDetections = useRunOnJS(
47
47
  (payload: WorkletPayload) => {
48
48
  const candidates = buildVinCandidates(payload, resolvedOptions);
49
- const barcodeCandidates = candidates.filter((c) => c.source === 'barcode');
49
+ const barcodeCandidates = candidates.filter(
50
+ (c) => c.source === 'barcode'
51
+ );
50
52
  const textCandidates = candidates.filter((c) => c.source === 'text');
51
53
 
52
54
  if (barcodeCandidates.length > 0) {
@@ -64,7 +66,10 @@ export const Camera = forwardRef(function Camera(
64
66
  return;
65
67
  }
66
68
 
67
- if (resolvedOptions.text.requireConfirmation && textCandidates.length > 0) {
69
+ if (
70
+ resolvedOptions.text.requireConfirmation &&
71
+ textCandidates.length > 0
72
+ ) {
68
73
  options?.onTextPending?.(textCandidates);
69
74
  return;
70
75
  }
@@ -94,19 +99,24 @@ export const Camera = forwardRef(function Camera(
94
99
  (frame) => {
95
100
  'worklet';
96
101
 
97
- const now = typeof frame?.timestamp === 'number' ? frame.timestamp : Date.now();
102
+ const now =
103
+ typeof frame?.timestamp === 'number' ? frame.timestamp : Date.now();
98
104
 
99
105
  // Max FPS throttle (matches hook behavior)
100
106
  const maxFps = resolvedOptions.detection.maxFrameRate;
101
107
  if (maxFps > 0) {
102
108
  const minInterval = 1000 / maxFps;
103
- if (lastFrameTimestampRef.current > 0 && now - lastFrameTimestampRef.current < minInterval) {
109
+ if (
110
+ lastFrameTimestampRef.current > 0 &&
111
+ now - lastFrameTimestampRef.current < minInterval
112
+ ) {
104
113
  return;
105
114
  }
106
115
  lastFrameTimestampRef.current = now;
107
116
  }
108
117
 
109
- const orientationOverride = resolvedOptions.detection.forceOrientation ?? null;
118
+ const orientationOverride =
119
+ resolvedOptions.detection.forceOrientation ?? null;
110
120
  const regionOverride = resolvedOptions.detection.scanRegion ?? null;
111
121
 
112
122
  // Frame quality gate (luma + sharpness)
@@ -158,28 +168,34 @@ export const Camera = forwardRef(function Camera(
158
168
  const pluginArgs =
159
169
  orientationOverride || regionOverride
160
170
  ? {
161
- ...(orientationOverride && { orientation: orientationOverride }),
162
- ...(regionOverride && { scanRegion: regionOverride }),
163
- }
171
+ ...(orientationOverride && { orientation: orientationOverride }),
172
+ ...(regionOverride && { scanRegion: regionOverride }),
173
+ }
164
174
  : undefined;
165
175
 
166
176
  // Run barcode every frame
167
177
  const barcodes = scanBarcodes
168
178
  ? scanBarcodes(
169
- frame,
170
- useBarcodeFallback.current
171
- ? { all: true }
172
- : pluginArgs
173
- )
179
+ frame,
180
+ useBarcodeFallback.current ? { all: true } : pluginArgs
181
+ )
174
182
  : [];
175
183
 
176
184
  // Run text every Nth frame per textScanInterval
177
185
  frameCounterRef.current += 1;
178
186
  const frameIndex = frameCounterRef.current;
179
187
  const textScanInterval = resolvedOptions.detection.textScanInterval;
180
- const shouldRunText = scanText && (textScanInterval <= 1 || frameIndex % textScanInterval === 0);
181
- let textBlocks = shouldRunText && scanText ? scanText(frame, pluginArgs) ?? [] : [];
182
- if (scanText && (textBlocks ?? []).length === 0 && (barcodes ?? []).length === 0 && !shouldRunText) {
188
+ const shouldRunText =
189
+ scanText &&
190
+ (textScanInterval <= 1 || frameIndex % textScanInterval === 0);
191
+ let textBlocks =
192
+ shouldRunText && scanText ? (scanText(frame, pluginArgs) ?? []) : [];
193
+ if (
194
+ scanText &&
195
+ (textBlocks ?? []).length === 0 &&
196
+ (barcodes ?? []).length === 0 &&
197
+ !shouldRunText
198
+ ) {
183
199
  textBlocks = scanText(frame, pluginArgs) ?? [];
184
200
  }
185
201
 
@@ -198,7 +214,10 @@ export const Camera = forwardRef(function Camera(
198
214
  const hasBarcode = (payload.barcodes ?? []).length > 0;
199
215
  if (!hasBarcode) {
200
216
  noBarcodeFrameCount.current += 1;
201
- if (noBarcodeFrameCount.current >= resolvedOptions.detection.barcodeFallbackAfter) {
217
+ if (
218
+ noBarcodeFrameCount.current >=
219
+ resolvedOptions.detection.barcodeFallbackAfter
220
+ ) {
202
221
  useBarcodeFallback.current = true;
203
222
  }
204
223
  } else {
@@ -210,7 +229,8 @@ export const Camera = forwardRef(function Camera(
210
229
  if (firstCandidate) {
211
230
  const debounceMs = resolvedOptions.duplicateDebounceMs ?? 1500;
212
231
  const isDuplicate =
213
- firstCandidate.value === lastEmittedVin.current && now - lastEmitTimestamp.current < debounceMs;
232
+ firstCandidate.value === lastEmittedVin.current &&
233
+ now - lastEmitTimestamp.current < debounceMs;
214
234
  const seen = sessionSeen.current.has(firstCandidate.value);
215
235
 
216
236
  if (isDuplicate || seen) {
@@ -237,9 +257,11 @@ export const Camera = forwardRef(function Camera(
237
257
  );
238
258
 
239
259
  // Create tap gesture for focus
240
- const tapGesture = Gesture.Tap().onEnd(({ x, y }: { x: number; y: number }) => {
241
- runOnJS(focus)({ x, y });
242
- });
260
+ const tapGesture = Gesture.Tap().onEnd(
261
+ ({ x, y }: { x: number; y: number }) => {
262
+ runOnJS(focus)({ x, y });
263
+ }
264
+ );
243
265
 
244
266
  return (
245
267
  <>
@@ -252,7 +274,9 @@ export const Camera = forwardRef(function Camera(
252
274
  device={device}
253
275
  enableZoomGesture={true}
254
276
  lowLightBoost={options?.cameraSettings?.lowLightBoost ?? true}
255
- videoStabilizationMode={options?.cameraSettings?.videoStabilizationMode ?? 'cinematic'}
277
+ videoStabilizationMode={
278
+ options?.cameraSettings?.videoStabilizationMode ?? 'cinematic'
279
+ }
256
280
  fps={Math.min(30, Math.max(24, options?.cameraSettings?.fps ?? 24))}
257
281
  {...rest}
258
282
  />
@@ -116,7 +116,11 @@ const mergeArgs = (
116
116
  'worklet';
117
117
  const merged: BarcodePluginArgs = { ...baseArgs };
118
118
  if (overrides) {
119
- const { orientation: overrideOrientation, scanRegion: overrideScanRegion, ...formatMap } = overrides;
119
+ const {
120
+ orientation: overrideOrientation,
121
+ scanRegion: overrideScanRegion,
122
+ ...formatMap
123
+ } = overrides;
120
124
  Object.entries(formatMap).forEach(([key, value]) => {
121
125
  merged[key as BarcodeFormat] = value as boolean | undefined;
122
126
  });
package/src/scanText.ts CHANGED
@@ -90,7 +90,11 @@ const mergeTextArgs = (
90
90
  ...base,
91
91
  };
92
92
  if (overrides) {
93
- const { orientation: overrideOrientation, scanRegion: overrideScanRegion, ...rest } = overrides;
93
+ const {
94
+ orientation: overrideOrientation,
95
+ scanRegion: overrideScanRegion,
96
+ ...rest
97
+ } = overrides;
94
98
  Object.assign(merged, rest);
95
99
  if (overrideScanRegion) {
96
100
  merged.scanRegion = overrideScanRegion;
package/src/types.ts CHANGED
@@ -102,7 +102,12 @@ export type BoundingBox = {
102
102
 
103
103
  export type VinCandidateSource = 'barcode' | 'text';
104
104
 
105
- export type VinTextOrigin = 'rawValue' | 'displayValue' | 'block' | 'line' | 'element';
105
+ export type VinTextOrigin =
106
+ | 'rawValue'
107
+ | 'displayValue'
108
+ | 'block'
109
+ | 'line'
110
+ | 'element';
106
111
 
107
112
  export type VinCandidate = {
108
113
  value: string;
@@ -112,9 +117,9 @@ export type VinCandidate = {
112
117
  origin?: VinTextOrigin;
113
118
  boundingBox?: BoundingBox;
114
119
  rawPayload?:
115
- | BarcodeDetection
116
- | TextDetection
117
- | { resultText?: string;[key: string]: unknown };
120
+ | BarcodeDetection
121
+ | TextDetection
122
+ | { resultText?: string; [key: string]: unknown };
118
123
  };
119
124
 
120
125
  export type VinScannerEvent = {
@@ -10,10 +10,7 @@ import {
10
10
  pickFirstCandidate,
11
11
  resolveOptions,
12
12
  } from './vinUtils';
13
- import {
14
- triggerSuccessHaptic,
15
- triggerSoftHaptic,
16
- } from './haptics';
13
+ import { triggerSuccessHaptic, triggerSoftHaptic } from './haptics';
17
14
 
18
15
  export function useVinScanner(options?: VinScannerOptions) {
19
16
  const resolvedOptions = useMemo(() => resolveOptions(options), [options]);
@@ -21,7 +18,9 @@ export function useVinScanner(options?: VinScannerOptions) {
21
18
  const frameCounterRef = useRef(0);
22
19
  const lastEmittedVin = useRef<string | null>(null);
23
20
  const lastEmitTimestamp = useRef(0);
24
- const [pendingTextCandidates, setPendingTextCandidates] = useState<VinCandidate[]>([]);
21
+ const [pendingTextCandidates, setPendingTextCandidates] = useState<
22
+ VinCandidate[]
23
+ >([]);
25
24
  const pendingTextTimestampRef = useRef<number | null>(null);
26
25
  const pendingTextRef = useRef<VinCandidate[]>([]);
27
26
  const pendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -149,33 +148,33 @@ export function useVinScanner(options?: VinScannerOptions) {
149
148
  }
150
149
  }
151
150
 
152
- const barcodeArgs = orientationOverride || regionOverride
153
- ? {
154
- ...(orientationOverride && { orientation: orientationOverride }),
155
- ...(regionOverride && { scanRegion: regionOverride })
156
- }
157
- : undefined;
151
+ const barcodeArgs =
152
+ orientationOverride || regionOverride
153
+ ? {
154
+ ...(orientationOverride && { orientation: orientationOverride }),
155
+ ...(regionOverride && { scanRegion: regionOverride }),
156
+ }
157
+ : undefined;
158
158
 
159
159
  // Start performance tracking
160
160
  const t0 = Date.now();
161
161
 
162
162
  const barcodes = barcodeScanner
163
- ? barcodeScanner.scanBarcodes(
164
- frame,
165
- useBarcodeFallback.current
166
- ? { all: true }
167
- : barcodeArgs
168
- ) ?? []
163
+ ? (barcodeScanner.scanBarcodes(
164
+ frame,
165
+ useBarcodeFallback.current ? { all: true } : barcodeArgs
166
+ ) ?? [])
169
167
  : [];
170
168
 
171
169
  const t1 = Date.now();
172
170
 
173
- const textArgs = orientationOverride || regionOverride
174
- ? {
175
- ...(orientationOverride && { orientation: orientationOverride }),
176
- ...(regionOverride && { scanRegion: regionOverride })
177
- }
178
- : undefined;
171
+ const textArgs =
172
+ orientationOverride || regionOverride
173
+ ? {
174
+ ...(orientationOverride && { orientation: orientationOverride }),
175
+ ...(regionOverride && { scanRegion: regionOverride }),
176
+ }
177
+ : undefined;
179
178
 
180
179
  frameCounterRef.current += 1;
181
180
  const frameIndex = frameCounterRef.current;
@@ -187,7 +186,7 @@ export function useVinScanner(options?: VinScannerOptions) {
187
186
 
188
187
  let textBlocks =
189
188
  shouldRunText && textScanner
190
- ? textScanner.scanText(frame, textArgs) ?? []
189
+ ? (textScanner.scanText(frame, textArgs) ?? [])
191
190
  : [];
192
191
 
193
192
  // Two-pass OCR: if we skipped this frame or got nothing and barcodes are empty, run one immediate OCR pass.
@@ -209,14 +208,19 @@ export function useVinScanner(options?: VinScannerOptions) {
209
208
  };
210
209
 
211
210
  const candidates = buildVinCandidates(payload, resolvedOptions);
212
- const filtered = candidates.filter((c) => c.confidence >= resolvedOptions.detection.minConfidence);
211
+ const filtered = candidates.filter(
212
+ (c) => c.confidence >= resolvedOptions.detection.minConfidence
213
+ );
213
214
  const barcodeCandidates = filtered.filter((c) => c.source === 'barcode');
214
215
  const textCandidates = filtered.filter((c) => c.source === 'text');
215
216
 
216
217
  // Adaptive barcode fallback: if no barcode hits for N frames, scan all formats.
217
218
  if (barcodeCandidates.length === 0) {
218
219
  noBarcodeFrameCount.current += 1;
219
- if (noBarcodeFrameCount.current >= resolvedOptions.detection.barcodeFallbackAfter) {
220
+ if (
221
+ noBarcodeFrameCount.current >=
222
+ resolvedOptions.detection.barcodeFallbackAfter
223
+ ) {
220
224
  useBarcodeFallback.current = true;
221
225
  }
222
226
  } else {
@@ -248,7 +252,10 @@ export function useVinScanner(options?: VinScannerOptions) {
248
252
 
249
253
  // If a barcode is present, emit immediately (highest confidence path)
250
254
  if (barcodeCandidates.length > 0) {
251
- const barcodeFirst = pickFirstCandidate(barcodeCandidates, resolvedOptions);
255
+ const barcodeFirst = pickFirstCandidate(
256
+ barcodeCandidates,
257
+ resolvedOptions
258
+ );
252
259
  if (barcodeFirst) {
253
260
  const debounceMs = resolvedOptions.duplicateDebounceMs ?? 1500;
254
261
  const isDuplicate =
@@ -273,7 +280,10 @@ export function useVinScanner(options?: VinScannerOptions) {
273
280
  }
274
281
 
275
282
  // For text-only detections, optionally require user confirmation
276
- if (resolvedOptions.text.requireConfirmation && textCandidates.length > 0) {
283
+ if (
284
+ resolvedOptions.text.requireConfirmation &&
285
+ textCandidates.length > 0
286
+ ) {
277
287
  emitTextPending(textCandidates, payload.timestamp);
278
288
  return;
279
289
  }