react-native-rectangle-doc-scanner 3.121.0 → 3.123.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.
@@ -125,6 +125,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
125
125
  const [rotationDegrees, setRotationDegrees] = (0, react_1.useState)(0);
126
126
  const [capturedPhotos, setCapturedPhotos] = (0, react_1.useState)([]);
127
127
  const [currentPhotoIndex, setCurrentPhotoIndex] = (0, react_1.useState)(0);
128
+ const [scannerSession, setScannerSession] = (0, react_1.useState)(0);
128
129
  const resolvedGridColor = gridColor ?? overlayColor;
129
130
  const docScannerRef = (0, react_1.useRef)(null);
130
131
  const captureModeRef = (0, react_1.useRef)(null);
@@ -132,6 +133,29 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
132
133
  const rectangleCaptureTimeoutRef = (0, react_1.useRef)(null);
133
134
  const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
134
135
  const isBusinessMode = type === 'business';
136
+ const resetScannerView = (0, react_1.useCallback)((options) => {
137
+ setProcessing(false);
138
+ setCroppedImageData(null);
139
+ setRotationDegrees(0);
140
+ setRectangleDetected(false);
141
+ setRectangleHint(false);
142
+ captureModeRef.current = null;
143
+ captureInProgressRef.current = false;
144
+ if (rectangleCaptureTimeoutRef.current) {
145
+ clearTimeout(rectangleCaptureTimeoutRef.current);
146
+ rectangleCaptureTimeoutRef.current = null;
147
+ }
148
+ if (rectangleHintTimeoutRef.current) {
149
+ clearTimeout(rectangleHintTimeoutRef.current);
150
+ rectangleHintTimeoutRef.current = null;
151
+ }
152
+ if (docScannerRef.current?.reset) {
153
+ docScannerRef.current.reset();
154
+ }
155
+ if (options?.remount) {
156
+ setScannerSession((prev) => prev + 1);
157
+ }
158
+ }, []);
135
159
  const mergedStrings = (0, react_1.useMemo)(() => ({
136
160
  captureHint: strings?.captureHint,
137
161
  manualHint: strings?.manualHint,
@@ -191,37 +215,28 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
191
215
  });
192
216
  }
193
217
  catch (error) {
194
- console.error('[FullDocScanner] openCropper error:', error);
195
- setProcessing(false);
196
- // Reset capture state when cropper fails or is cancelled
197
- captureInProgressRef.current = false;
198
- captureModeRef.current = null;
199
- setRectangleDetected(false);
200
- setRectangleHint(false);
201
- if (docScannerRef.current?.reset) {
202
- docScannerRef.current.reset();
203
- }
218
+ resetScannerView({ remount: true });
204
219
  const errorCode = error?.code;
205
- const errorMessage = error?.message || String(error);
220
+ const errorMessageRaw = error?.message ?? String(error);
221
+ const errorMessage = typeof errorMessageRaw === 'string' ? errorMessageRaw : String(errorMessageRaw);
222
+ const normalizedMessage = errorMessage.toLowerCase();
223
+ const isUserCancelled = errorCode === 'E_PICKER_CANCELLED' ||
224
+ normalizedMessage === 'user cancelled image selection' ||
225
+ normalizedMessage.includes('cancel');
226
+ if (isUserCancelled) {
227
+ console.log('[FullDocScanner] User cancelled cropper');
228
+ return;
229
+ }
230
+ console.error('[FullDocScanner] openCropper error:', error);
206
231
  if (errorCode === CROPPER_TIMEOUT_CODE || errorMessage === CROPPER_TIMEOUT_CODE) {
207
232
  console.error('[FullDocScanner] Cropper timed out waiting for presentation');
208
233
  emitError(error instanceof Error ? error : new Error('Cropper timed out'), 'Failed to open crop editor. Please try again.');
209
234
  }
210
- else if (errorCode === 'E_PICKER_CANCELLED' ||
211
- errorMessage === 'User cancelled image selection' ||
212
- errorMessage.includes('cancelled') ||
213
- errorMessage.includes('cancel')) {
214
- console.log('[FullDocScanner] User cancelled cropper');
215
- // DocScanner 상태를 리셋하여 카메라가 다시 작동하도록 함
216
- if (docScannerRef.current?.reset) {
217
- docScannerRef.current.reset();
218
- }
219
- }
220
235
  else {
221
236
  emitError(error instanceof Error ? error : new Error(errorMessage), 'Failed to crop image. Please try again.');
222
237
  }
223
238
  }
224
- }, [cropWidth, cropHeight, emitError]);
239
+ }, [cropWidth, cropHeight, emitError, resetScannerView]);
225
240
  const handleCapture = (0, react_1.useCallback)(async (document) => {
226
241
  console.log('[FullDocScanner] handleCapture called:', {
227
242
  origin: document.origin,
@@ -420,25 +435,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
420
435
  setCapturedPhotos([currentPhoto]);
421
436
  setCurrentPhotoIndex(1);
422
437
  // 확인 화면을 닫고 카메라로 돌아감
423
- setCroppedImageData(null);
424
- setRotationDegrees(0);
425
- setProcessing(false);
426
- setRectangleDetected(false);
427
- setRectangleHint(false);
428
- captureModeRef.current = null;
429
- captureInProgressRef.current = false;
430
- if (rectangleCaptureTimeoutRef.current) {
431
- clearTimeout(rectangleCaptureTimeoutRef.current);
432
- rectangleCaptureTimeoutRef.current = null;
433
- }
434
- if (rectangleHintTimeoutRef.current) {
435
- clearTimeout(rectangleHintTimeoutRef.current);
436
- rectangleHintTimeoutRef.current = null;
437
- }
438
- if (docScannerRef.current?.reset) {
439
- docScannerRef.current.reset();
440
- }
441
- }, [croppedImageData, rotationDegrees]);
438
+ resetScannerView({ remount: true });
439
+ }, [croppedImageData, resetScannerView, rotationDegrees]);
442
440
  const handleRetake = (0, react_1.useCallback)(() => {
443
441
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
444
442
  // Business 모드에서 두 번째 사진을 다시 찍는 경우, 첫 번째 사진 유지
@@ -452,26 +450,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
452
450
  setCapturedPhotos([]);
453
451
  setCurrentPhotoIndex(0);
454
452
  }
455
- setCroppedImageData(null);
456
- setRotationDegrees(0);
457
- setProcessing(false);
458
- setRectangleDetected(false);
459
- setRectangleHint(false);
460
- captureModeRef.current = null;
461
- captureInProgressRef.current = false;
462
- if (rectangleCaptureTimeoutRef.current) {
463
- clearTimeout(rectangleCaptureTimeoutRef.current);
464
- rectangleCaptureTimeoutRef.current = null;
465
- }
466
- if (rectangleHintTimeoutRef.current) {
467
- clearTimeout(rectangleHintTimeoutRef.current);
468
- rectangleHintTimeoutRef.current = null;
469
- }
470
- // Reset DocScanner state
471
- if (docScannerRef.current?.reset) {
472
- docScannerRef.current.reset();
473
- }
474
- }, [capturedPhotos.length, isBusinessMode]);
453
+ resetScannerView({ remount: true });
454
+ }, [capturedPhotos.length, isBusinessMode, resetScannerView]);
475
455
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
476
456
  const stableCounter = event.stableCounter ?? 0;
477
457
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
@@ -549,7 +529,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
549
529
  react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.retake)),
550
530
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleConfirm, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button" },
551
531
  react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.confirm))))) : (react_1.default.createElement(react_native_1.View, { style: styles.flex },
552
- react_1.default.createElement(DocScanner_1.DocScanner, { ref: docScannerRef, autoCapture: false, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture, onRectangleDetect: handleRectangleDetect, showManualCaptureButton: false, enableTorch: flashEnabled },
532
+ react_1.default.createElement(DocScanner_1.DocScanner, { key: scannerSession, ref: docScannerRef, autoCapture: false, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture, onRectangleDetect: handleRectangleDetect, showManualCaptureButton: false, enableTorch: flashEnabled },
553
533
  react_1.default.createElement(react_native_1.View, { style: styles.overlayTop, pointerEvents: "box-none" },
554
534
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
555
535
  styles.iconButton,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.121.0",
3
+ "version": "3.123.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -183,6 +183,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
183
183
  const [rotationDegrees, setRotationDegrees] = useState(0);
184
184
  const [capturedPhotos, setCapturedPhotos] = useState<FullDocScannerResult[]>([]);
185
185
  const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
186
+ const [scannerSession, setScannerSession] = useState(0);
186
187
  const resolvedGridColor = gridColor ?? overlayColor;
187
188
  const docScannerRef = useRef<DocScannerHandle | null>(null);
188
189
  const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
@@ -192,6 +193,37 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
192
193
 
193
194
  const isBusinessMode = type === 'business';
194
195
 
196
+ const resetScannerView = useCallback(
197
+ (options?: { remount?: boolean }) => {
198
+ setProcessing(false);
199
+ setCroppedImageData(null);
200
+ setRotationDegrees(0);
201
+ setRectangleDetected(false);
202
+ setRectangleHint(false);
203
+ captureModeRef.current = null;
204
+ captureInProgressRef.current = false;
205
+
206
+ if (rectangleCaptureTimeoutRef.current) {
207
+ clearTimeout(rectangleCaptureTimeoutRef.current);
208
+ rectangleCaptureTimeoutRef.current = null;
209
+ }
210
+
211
+ if (rectangleHintTimeoutRef.current) {
212
+ clearTimeout(rectangleHintTimeoutRef.current);
213
+ rectangleHintTimeoutRef.current = null;
214
+ }
215
+
216
+ if (docScannerRef.current?.reset) {
217
+ docScannerRef.current.reset();
218
+ }
219
+
220
+ if (options?.remount) {
221
+ setScannerSession((prev) => prev + 1);
222
+ }
223
+ },
224
+ [],
225
+ );
226
+
195
227
  const mergedStrings = useMemo(
196
228
  () => ({
197
229
  captureHint: strings?.captureHint,
@@ -268,20 +300,24 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
268
300
  base64: croppedImage.data ?? undefined,
269
301
  });
270
302
  } catch (error) {
271
- console.error('[FullDocScanner] openCropper error:', error);
272
- setProcessing(false);
303
+ resetScannerView({ remount: true });
273
304
 
274
- // Reset capture state when cropper fails or is cancelled
275
- captureInProgressRef.current = false;
276
- captureModeRef.current = null;
277
- setRectangleDetected(false);
278
- setRectangleHint(false);
279
- if (docScannerRef.current?.reset) {
280
- docScannerRef.current.reset();
305
+ const errorCode = (error as any)?.code;
306
+ const errorMessageRaw = (error as any)?.message ?? String(error);
307
+ const errorMessage =
308
+ typeof errorMessageRaw === 'string' ? errorMessageRaw : String(errorMessageRaw);
309
+ const normalizedMessage = errorMessage.toLowerCase();
310
+ const isUserCancelled =
311
+ errorCode === 'E_PICKER_CANCELLED' ||
312
+ normalizedMessage === 'user cancelled image selection' ||
313
+ normalizedMessage.includes('cancel');
314
+
315
+ if (isUserCancelled) {
316
+ console.log('[FullDocScanner] User cancelled cropper');
317
+ return;
281
318
  }
282
319
 
283
- const errorCode = (error as any)?.code;
284
- const errorMessage = (error as any)?.message || String(error);
320
+ console.error('[FullDocScanner] openCropper error:', error);
285
321
 
286
322
  if (errorCode === CROPPER_TIMEOUT_CODE || errorMessage === CROPPER_TIMEOUT_CODE) {
287
323
  console.error('[FullDocScanner] Cropper timed out waiting for presentation');
@@ -289,17 +325,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
289
325
  error instanceof Error ? error : new Error('Cropper timed out'),
290
326
  'Failed to open crop editor. Please try again.',
291
327
  );
292
- } else if (
293
- errorCode === 'E_PICKER_CANCELLED' ||
294
- errorMessage === 'User cancelled image selection' ||
295
- errorMessage.includes('cancelled') ||
296
- errorMessage.includes('cancel')
297
- ) {
298
- console.log('[FullDocScanner] User cancelled cropper');
299
- // DocScanner 상태를 리셋하여 카메라가 다시 작동하도록 함
300
- if (docScannerRef.current?.reset) {
301
- docScannerRef.current.reset();
302
- }
303
328
  } else {
304
329
  emitError(
305
330
  error instanceof Error ? error : new Error(errorMessage),
@@ -308,7 +333,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
308
333
  }
309
334
  }
310
335
  },
311
- [cropWidth, cropHeight, emitError],
336
+ [cropWidth, cropHeight, emitError, resetScannerView],
312
337
  );
313
338
 
314
339
  const handleCapture = useCallback(
@@ -563,25 +588,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
563
588
  setCurrentPhotoIndex(1);
564
589
 
565
590
  // 확인 화면을 닫고 카메라로 돌아감
566
- setCroppedImageData(null);
567
- setRotationDegrees(0);
568
- setProcessing(false);
569
- setRectangleDetected(false);
570
- setRectangleHint(false);
571
- captureModeRef.current = null;
572
- captureInProgressRef.current = false;
573
- if (rectangleCaptureTimeoutRef.current) {
574
- clearTimeout(rectangleCaptureTimeoutRef.current);
575
- rectangleCaptureTimeoutRef.current = null;
576
- }
577
- if (rectangleHintTimeoutRef.current) {
578
- clearTimeout(rectangleHintTimeoutRef.current);
579
- rectangleHintTimeoutRef.current = null;
580
- }
581
- if (docScannerRef.current?.reset) {
582
- docScannerRef.current.reset();
583
- }
584
- }, [croppedImageData, rotationDegrees]);
591
+ resetScannerView({ remount: true });
592
+ }, [croppedImageData, resetScannerView, rotationDegrees]);
585
593
 
586
594
 
587
595
  const handleRetake = useCallback(() => {
@@ -598,26 +606,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
598
606
  setCurrentPhotoIndex(0);
599
607
  }
600
608
 
601
- setCroppedImageData(null);
602
- setRotationDegrees(0);
603
- setProcessing(false);
604
- setRectangleDetected(false);
605
- setRectangleHint(false);
606
- captureModeRef.current = null;
607
- captureInProgressRef.current = false;
608
- if (rectangleCaptureTimeoutRef.current) {
609
- clearTimeout(rectangleCaptureTimeoutRef.current);
610
- rectangleCaptureTimeoutRef.current = null;
611
- }
612
- if (rectangleHintTimeoutRef.current) {
613
- clearTimeout(rectangleHintTimeoutRef.current);
614
- rectangleHintTimeoutRef.current = null;
615
- }
616
- // Reset DocScanner state
617
- if (docScannerRef.current?.reset) {
618
- docScannerRef.current.reset();
619
- }
620
- }, [capturedPhotos.length, isBusinessMode]);
609
+ resetScannerView({ remount: true });
610
+ }, [capturedPhotos.length, isBusinessMode, resetScannerView]);
621
611
 
622
612
  const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
623
613
  const stableCounter = event.stableCounter ?? 0;
@@ -780,6 +770,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
780
770
  ) : (
781
771
  <View style={styles.flex}>
782
772
  <DocScanner
773
+ key={scannerSession}
783
774
  ref={docScannerRef}
784
775
  autoCapture={false}
785
776
  overlayColor={overlayColor}