react-native-rectangle-doc-scanner 3.68.0 → 3.70.0

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.
@@ -58,11 +58,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
58
58
  const [croppedImageData, setCroppedImageData] = (0, react_1.useState)(null);
59
59
  const [isGalleryOpen, setIsGalleryOpen] = (0, react_1.useState)(false);
60
60
  const [rectangleDetected, setRectangleDetected] = (0, react_1.useState)(false);
61
+ const [rectangleHint, setRectangleHint] = (0, react_1.useState)(false);
61
62
  const resolvedGridColor = gridColor ?? overlayColor;
62
63
  const docScannerRef = (0, react_1.useRef)(null);
63
64
  const captureModeRef = (0, react_1.useRef)(null);
64
65
  const captureInProgressRef = (0, react_1.useRef)(false);
65
- const rectangleTimeoutRef = (0, react_1.useRef)(null);
66
+ const rectangleCaptureTimeoutRef = (0, react_1.useRef)(null);
67
+ const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
66
68
  const mergedStrings = (0, react_1.useMemo)(() => ({
67
69
  captureHint: strings?.captureHint,
68
70
  manualHint: strings?.manualHint,
@@ -166,6 +168,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
166
168
  processing,
167
169
  hasRef: hasScanner,
168
170
  rectangleDetected,
171
+ rectangleHint,
169
172
  currentCaptureMode: captureModeRef.current,
170
173
  captureInProgress: captureInProgressRef.current,
171
174
  });
@@ -214,7 +217,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
214
217
  emitError(error, 'Failed to capture image. Please try again.');
215
218
  }
216
219
  });
217
- }, [processing, rectangleDetected, emitError]);
220
+ }, [processing, rectangleDetected, rectangleHint, emitError]);
218
221
  const handleGalleryPick = (0, react_1.useCallback)(async () => {
219
222
  console.log('[FullDocScanner] handleGalleryPick called');
220
223
  if (processing || isGalleryOpen) {
@@ -266,11 +269,16 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
266
269
  setCroppedImageData(null);
267
270
  setProcessing(false);
268
271
  setRectangleDetected(false);
272
+ setRectangleHint(false);
269
273
  captureModeRef.current = null;
270
274
  captureInProgressRef.current = false;
271
- if (rectangleTimeoutRef.current) {
272
- clearTimeout(rectangleTimeoutRef.current);
273
- rectangleTimeoutRef.current = null;
275
+ if (rectangleCaptureTimeoutRef.current) {
276
+ clearTimeout(rectangleCaptureTimeoutRef.current);
277
+ rectangleCaptureTimeoutRef.current = null;
278
+ }
279
+ if (rectangleHintTimeoutRef.current) {
280
+ clearTimeout(rectangleHintTimeoutRef.current);
281
+ rectangleHintTimeoutRef.current = null;
274
282
  }
275
283
  // Reset DocScanner state
276
284
  if (docScannerRef.current?.reset) {
@@ -279,40 +287,62 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
279
287
  }, []);
280
288
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
281
289
  const stableCounter = event.stableCounter ?? 0;
282
- const hasRectangle = Boolean(event.rectangleOnScreen ?? event.rectangleCoordinates);
283
- const isGoodRectangle = hasRectangle && event.lastDetectionType === 0;
284
- // Clear timeout immediately when rectangle is lost
285
- if (!hasRectangle || !isGoodRectangle) {
286
- if (rectangleTimeoutRef.current) {
287
- clearTimeout(rectangleTimeoutRef.current);
288
- rectangleTimeoutRef.current = null;
290
+ const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
291
+ const hasRectangle = Boolean(rectangleCoordinates);
292
+ const captureReady = hasRectangle && event.lastDetectionType === 0 && stableCounter >= 1;
293
+ const scheduleClear = (ref, clearFn) => {
294
+ if (ref.current) {
295
+ clearTimeout(ref.current);
289
296
  }
290
- setRectangleDetected(false);
297
+ ref.current = setTimeout(() => {
298
+ ref.current = null;
299
+ clearFn();
300
+ }, 350);
301
+ };
302
+ if (hasRectangle) {
303
+ scheduleClear(rectangleHintTimeoutRef, () => setRectangleHint(false));
304
+ setRectangleHint(true);
291
305
  }
292
306
  else {
293
- // Rectangle detected - clear any existing timeout
294
- if (rectangleTimeoutRef.current) {
295
- clearTimeout(rectangleTimeoutRef.current);
307
+ if (rectangleHintTimeoutRef.current) {
308
+ clearTimeout(rectangleHintTimeoutRef.current);
309
+ rectangleHintTimeoutRef.current = null;
296
310
  }
297
- setRectangleDetected(true);
298
- // Set timeout to clear rectangle after brief period of no updates
299
- rectangleTimeoutRef.current = setTimeout(() => {
300
- rectangleTimeoutRef.current = null;
311
+ setRectangleHint(false);
312
+ }
313
+ if (captureReady) {
314
+ scheduleClear(rectangleCaptureTimeoutRef, () => {
315
+ console.log('[FullDocScanner] Rectangle timeout - clearing detection');
301
316
  setRectangleDetected(false);
317
+ });
318
+ setRectangleDetected(true);
319
+ }
320
+ else if (!hasRectangle) {
321
+ if (rectangleCaptureTimeoutRef.current) {
322
+ clearTimeout(rectangleCaptureTimeoutRef.current);
323
+ rectangleCaptureTimeoutRef.current = null;
324
+ }
325
+ setRectangleDetected(false);
326
+ }
327
+ else if (rectangleDetected) {
328
+ scheduleClear(rectangleCaptureTimeoutRef, () => {
302
329
  console.log('[FullDocScanner] Rectangle timeout - clearing detection');
303
- }, 300);
330
+ setRectangleDetected(false);
331
+ });
304
332
  }
305
333
  console.log('[FullDocScanner] Rectangle detection update', {
306
334
  lastDetectionType: event.lastDetectionType,
307
335
  stableCounter,
308
336
  hasRectangle,
309
- isGoodRectangle,
310
- rectangleDetected: isGoodRectangle,
337
+ captureReady,
311
338
  });
312
- }, []);
339
+ }, [rectangleDetected]);
313
340
  (0, react_1.useEffect)(() => () => {
314
- if (rectangleTimeoutRef.current) {
315
- clearTimeout(rectangleTimeoutRef.current);
341
+ if (rectangleCaptureTimeoutRef.current) {
342
+ clearTimeout(rectangleCaptureTimeoutRef.current);
343
+ }
344
+ if (rectangleHintTimeoutRef.current) {
345
+ clearTimeout(rectangleHintTimeoutRef.current);
316
346
  }
317
347
  }, []);
318
348
  return (react_1.default.createElement(react_native_1.View, { style: styles.container },
@@ -339,7 +369,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
339
369
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.shutterButton, processing && styles.buttonDisabled], onPress: triggerManualCapture, disabled: processing, accessibilityLabel: mergedStrings.manualHint, accessibilityRole: "button" },
340
370
  react_1.default.createElement(react_native_1.View, { style: [
341
371
  styles.shutterInner,
342
- rectangleDetected && { backgroundColor: overlayColor }
372
+ rectangleHint && { backgroundColor: overlayColor }
343
373
  ] })))))),
344
374
  processing && (react_1.default.createElement(react_native_1.View, { style: styles.processingOverlay },
345
375
  react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: overlayColor }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.68.0",
3
+ "version": "3.70.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -86,11 +86,13 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
86
86
  const [croppedImageData, setCroppedImageData] = useState<{path: string; base64?: string} | null>(null);
87
87
  const [isGalleryOpen, setIsGalleryOpen] = useState(false);
88
88
  const [rectangleDetected, setRectangleDetected] = useState(false);
89
+ const [rectangleHint, setRectangleHint] = useState(false);
89
90
  const resolvedGridColor = gridColor ?? overlayColor;
90
91
  const docScannerRef = useRef<DocScannerHandle | null>(null);
91
92
  const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
92
93
  const captureInProgressRef = useRef(false);
93
- const rectangleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
94
+ const rectangleCaptureTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
95
+ const rectangleHintTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
94
96
 
95
97
  const mergedStrings = useMemo(
96
98
  () => ({
@@ -226,6 +228,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
226
228
  processing,
227
229
  hasRef: hasScanner,
228
230
  rectangleDetected,
231
+ rectangleHint,
229
232
  currentCaptureMode: captureModeRef.current,
230
233
  captureInProgress: captureInProgressRef.current,
231
234
  });
@@ -288,7 +291,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
288
291
  );
289
292
  }
290
293
  });
291
- }, [processing, rectangleDetected, emitError]);
294
+ }, [processing, rectangleDetected, rectangleHint, emitError]);
292
295
 
293
296
  const handleGalleryPick = useCallback(async () => {
294
297
  console.log('[FullDocScanner] handleGalleryPick called');
@@ -353,11 +356,16 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
353
356
  setCroppedImageData(null);
354
357
  setProcessing(false);
355
358
  setRectangleDetected(false);
359
+ setRectangleHint(false);
356
360
  captureModeRef.current = null;
357
361
  captureInProgressRef.current = false;
358
- if (rectangleTimeoutRef.current) {
359
- clearTimeout(rectangleTimeoutRef.current);
360
- rectangleTimeoutRef.current = null;
362
+ if (rectangleCaptureTimeoutRef.current) {
363
+ clearTimeout(rectangleCaptureTimeoutRef.current);
364
+ rectangleCaptureTimeoutRef.current = null;
365
+ }
366
+ if (rectangleHintTimeoutRef.current) {
367
+ clearTimeout(rectangleHintTimeoutRef.current);
368
+ rectangleHintTimeoutRef.current = null;
361
369
  }
362
370
  // Reset DocScanner state
363
371
  if (docScannerRef.current?.reset) {
@@ -367,45 +375,72 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
367
375
 
368
376
  const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
369
377
  const stableCounter = event.stableCounter ?? 0;
370
- const hasRectangle = Boolean(event.rectangleOnScreen ?? event.rectangleCoordinates);
371
- const isGoodRectangle = hasRectangle && event.lastDetectionType === 0;
372
-
373
- // Clear timeout immediately when rectangle is lost
374
- if (!hasRectangle || !isGoodRectangle) {
375
- if (rectangleTimeoutRef.current) {
376
- clearTimeout(rectangleTimeoutRef.current);
377
- rectangleTimeoutRef.current = null;
378
+ const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
379
+ const hasRectangle = Boolean(rectangleCoordinates);
380
+ const captureReady = hasRectangle && event.lastDetectionType === 0 && stableCounter >= 1;
381
+
382
+ const scheduleClear = (
383
+ ref: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
384
+ clearFn: () => void,
385
+ ) => {
386
+ if (ref.current) {
387
+ clearTimeout(ref.current);
378
388
  }
379
- setRectangleDetected(false);
389
+ ref.current = setTimeout(() => {
390
+ ref.current = null;
391
+ clearFn();
392
+ }, 350);
393
+ };
394
+
395
+ if (hasRectangle) {
396
+ scheduleClear(rectangleHintTimeoutRef, () => setRectangleHint(false));
397
+ setRectangleHint(true);
380
398
  } else {
381
- // Rectangle detected - clear any existing timeout
382
- if (rectangleTimeoutRef.current) {
383
- clearTimeout(rectangleTimeoutRef.current);
399
+ if (rectangleHintTimeoutRef.current) {
400
+ clearTimeout(rectangleHintTimeoutRef.current);
401
+ rectangleHintTimeoutRef.current = null;
384
402
  }
385
- setRectangleDetected(true);
403
+ setRectangleHint(false);
404
+ }
386
405
 
387
- // Set timeout to clear rectangle after brief period of no updates
388
- rectangleTimeoutRef.current = setTimeout(() => {
389
- rectangleTimeoutRef.current = null;
406
+ if (captureReady) {
407
+ scheduleClear(rectangleCaptureTimeoutRef, () => {
408
+ console.log('[FullDocScanner] Rectangle timeout - clearing detection');
390
409
  setRectangleDetected(false);
410
+ });
411
+ setRectangleDetected(true);
412
+ } else if (!hasRectangle) {
413
+ if (rectangleCaptureTimeoutRef.current) {
414
+ clearTimeout(rectangleCaptureTimeoutRef.current);
415
+ rectangleCaptureTimeoutRef.current = null;
416
+ }
417
+ setRectangleDetected(false);
418
+ } else if (rectangleDetected) {
419
+ scheduleClear(rectangleCaptureTimeoutRef, () => {
391
420
  console.log('[FullDocScanner] Rectangle timeout - clearing detection');
392
- }, 300);
421
+ setRectangleDetected(false);
422
+ });
393
423
  }
394
424
 
395
425
  console.log('[FullDocScanner] Rectangle detection update', {
396
426
  lastDetectionType: event.lastDetectionType,
397
427
  stableCounter,
398
428
  hasRectangle,
399
- isGoodRectangle,
400
- rectangleDetected: isGoodRectangle,
429
+ captureReady,
401
430
  });
402
- }, []);
431
+ }, [rectangleDetected]);
403
432
 
404
- useEffect(() => () => {
405
- if (rectangleTimeoutRef.current) {
406
- clearTimeout(rectangleTimeoutRef.current);
407
- }
408
- }, []);
433
+ useEffect(
434
+ () => () => {
435
+ if (rectangleCaptureTimeoutRef.current) {
436
+ clearTimeout(rectangleCaptureTimeoutRef.current);
437
+ }
438
+ if (rectangleHintTimeoutRef.current) {
439
+ clearTimeout(rectangleHintTimeoutRef.current);
440
+ }
441
+ },
442
+ [],
443
+ );
409
444
 
410
445
  return (
411
446
  <View style={styles.container}>
@@ -494,7 +529,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
494
529
  >
495
530
  <View style={[
496
531
  styles.shutterInner,
497
- rectangleDetected && { backgroundColor: overlayColor }
532
+ rectangleHint && { backgroundColor: overlayColor }
498
533
  ]} />
499
534
  </TouchableOpacity>
500
535
  </View>
@@ -35,22 +35,27 @@ RCT_EXPORT_VIEW_PROPERTY(brightness, float)
35
35
  RCT_EXPORT_VIEW_PROPERTY(contrast, float)
36
36
 
37
37
  // Main capture method - returns a Promise
38
- RCT_EXPORT_METHOD(capture:(nullable NSNumber *)reactTag
38
+ RCT_EXPORT_METHOD(capture:(nullable id)reactTag
39
39
  resolver:(RCTPromiseResolveBlock)resolve
40
40
  rejecter:(RCTPromiseRejectBlock)reject) {
41
41
  NSLog(@"[RNPdfScannerManager] capture called with reactTag: %@", reactTag);
42
42
  dispatch_async(dispatch_get_main_queue(), ^{
43
43
  DocumentScannerView *targetView = nil;
44
+ NSNumber *resolvedTag = ([reactTag isKindOfClass:[NSNumber class]]) ? (NSNumber *)reactTag : nil;
44
45
 
45
- if (reactTag) {
46
- UIView *view = [self.bridge.uiManager viewForReactTag:reactTag];
46
+ if (!resolvedTag && reactTag) {
47
+ NSLog(@"[RNPdfScannerManager] Unexpected reactTag type %@ - ignoring reactTag", NSStringFromClass([reactTag class]));
48
+ }
49
+
50
+ if (resolvedTag) {
51
+ UIView *view = [self.bridge.uiManager viewForReactTag:resolvedTag];
47
52
  if ([view isKindOfClass:[DocumentScannerView class]]) {
48
53
  targetView = (DocumentScannerView *)view;
49
54
  self->_scannerView = targetView;
50
55
  } else if (view) {
51
- NSLog(@"[RNPdfScannerManager] View for tag %@ is not DocumentScannerView: %@", reactTag, NSStringFromClass(view.class));
56
+ NSLog(@"[RNPdfScannerManager] View for tag %@ is not DocumentScannerView: %@", resolvedTag, NSStringFromClass(view.class));
52
57
  } else {
53
- NSLog(@"[RNPdfScannerManager] No view found for tag %@", reactTag);
58
+ NSLog(@"[RNPdfScannerManager] No view found for tag %@", resolvedTag);
54
59
  }
55
60
  }
56
61
 
@@ -61,7 +66,9 @@ RCT_EXPORT_METHOD(capture:(nullable NSNumber *)reactTag
61
66
 
62
67
  if (!targetView) {
63
68
  NSLog(@"[RNPdfScannerManager] ERROR: No scanner view available for capture");
64
- reject(@"NO_VIEW", @"No scanner view available for capture", nil);
69
+ if (reject) {
70
+ reject(@"NO_VIEW", @"Document scanner view is not ready", nil);
71
+ }
65
72
  return;
66
73
  }
67
74
 
@@ -31,12 +31,12 @@ class PdfScanner extends React.Component {
31
31
  const handle = findNodeHandle(this.scannerRef.current);
32
32
  console.log('[PdfScanner/ios.js] node handle (reactTag):', handle);
33
33
 
34
- if (!handle) {
35
- console.error('[PdfScanner/ios.js] ERROR: No handle found for scanner ref');
36
- return Promise.reject(new Error('No handle found for scanner view'));
34
+ if (typeof handle !== 'number') {
35
+ const error = new Error('DocumentScanner native view is not ready');
36
+ console.error('[PdfScanner/ios.js] ERROR:', error.message);
37
+ return Promise.reject(error);
37
38
  }
38
39
 
39
- // Call native method with reactTag - now returns a Promise
40
40
  console.log('[PdfScanner/ios.js] Calling native capture with handle:', handle);
41
41
  return NativeModules.RNPdfScannerManager.capture(handle);
42
42
  }