react-native-rectangle-doc-scanner 3.114.0 → 3.115.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.
@@ -23,6 +23,7 @@ export interface FullDocScannerStrings {
23
23
  first?: string;
24
24
  second?: string;
25
25
  secondBtn?: string;
26
+ secondPrompt?: string;
26
27
  }
27
28
  export interface FullDocScannerProps {
28
29
  onResult: (results: FullDocScannerResult[]) => void;
@@ -125,14 +125,19 @@ 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 [previewPhotoIndex, setPreviewPhotoIndex] = (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);
131
132
  const captureInProgressRef = (0, react_1.useRef)(false);
132
133
  const rectangleCaptureTimeoutRef = (0, react_1.useRef)(null);
133
134
  const rectangleHintTimeoutRef = (0, react_1.useRef)(null);
135
+ const currentPhotoIndexRef = (0, react_1.useRef)(currentPhotoIndex);
134
136
  const isBusinessMode = type === 'business';
135
137
  const maxPhotos = isBusinessMode ? 2 : 1;
138
+ (0, react_1.useEffect)(() => {
139
+ currentPhotoIndexRef.current = currentPhotoIndex;
140
+ }, [currentPhotoIndex]);
136
141
  const mergedStrings = (0, react_1.useMemo)(() => ({
137
142
  captureHint: strings?.captureHint,
138
143
  manualHint: strings?.manualHint,
@@ -145,6 +150,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
145
150
  first: strings?.first ?? 'Front',
146
151
  second: strings?.second ?? 'Back',
147
152
  secondBtn: strings?.secondBtn ?? 'Capture Back Side?',
153
+ secondPrompt: strings?.secondPrompt ?? strings?.secondBtn ?? 'Capture Back Side?',
148
154
  }), [strings]);
149
155
  const emitError = (0, react_1.useCallback)((error, fallbackMessage) => {
150
156
  console.error('[FullDocScanner] error', error);
@@ -184,6 +190,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
184
190
  hasBase64: !!croppedImage.data,
185
191
  });
186
192
  setProcessing(false);
193
+ setPreviewPhotoIndex(currentPhotoIndexRef.current);
187
194
  // Show confirmation screen
188
195
  setCroppedImageData({
189
196
  path: croppedImage.path,
@@ -230,6 +237,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
230
237
  console.warn('[FullDocScanner] Missing capture mode for capture result, ignoring');
231
238
  return;
232
239
  }
240
+ const previewIndex = currentPhotoIndexRef.current;
241
+ setPreviewPhotoIndex(previewIndex);
233
242
  const normalizedDoc = normalizeCapturedDocument(document);
234
243
  if (captureMode === 'no-grid') {
235
244
  console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
@@ -433,6 +442,20 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
433
442
  }, [capturedPhotos, onResult]);
434
443
  const handleRetake = (0, react_1.useCallback)(() => {
435
444
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
445
+ if (isBusinessMode) {
446
+ if (currentPhotoIndex === 1 && previewPhotoIndex === 0 && capturedPhotos.length === 1) {
447
+ console.log('[FullDocScanner] Retake detected on front preview after confirmation - reverting to front capture');
448
+ setCapturedPhotos([]);
449
+ setCurrentPhotoIndex(0);
450
+ setPreviewPhotoIndex(0);
451
+ }
452
+ else if (previewPhotoIndex === 1 && capturedPhotos.length === 2) {
453
+ console.log('[FullDocScanner] Retake detected on back preview after final confirmation - removing back photo');
454
+ setCapturedPhotos(capturedPhotos.slice(0, 1));
455
+ setCurrentPhotoIndex(1);
456
+ setPreviewPhotoIndex(1);
457
+ }
458
+ }
436
459
  setCroppedImageData(null);
437
460
  setRotationDegrees(0);
438
461
  setProcessing(false);
@@ -452,7 +475,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
452
475
  if (docScannerRef.current?.reset) {
453
476
  docScannerRef.current.reset();
454
477
  }
455
- }, []);
478
+ }, [capturedPhotos, currentPhotoIndex, isBusinessMode, previewPhotoIndex]);
456
479
  const handleRectangleDetect = (0, react_1.useCallback)((event) => {
457
480
  const stableCounter = event.stableCounter ?? 0;
458
481
  const rectangleCoordinates = event.rectangleOnScreen ?? event.rectangleCoordinates;
@@ -506,7 +529,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
506
529
  // check_DP: Show confirmation screen
507
530
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
508
531
  isBusinessMode && (react_1.default.createElement(react_native_1.View, { style: styles.photoHeader },
509
- react_1.default.createElement(react_native_1.Text, { style: styles.photoHeaderText }, currentPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second))),
532
+ react_1.default.createElement(react_native_1.Text, { style: styles.photoHeaderText }, previewPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second))),
510
533
  isImageRotationSupported() ? (react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
511
534
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(-90), accessibilityLabel: "\uC67C\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
512
535
  react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
@@ -518,10 +541,18 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
518
541
  styles.previewImage,
519
542
  { transform: [{ rotate: `${rotationDegrees}deg` }] }
520
543
  ], resizeMode: "contain" }),
544
+ isBusinessMode &&
545
+ capturedPhotos.length === 1 &&
546
+ currentPhotoIndex === 1 &&
547
+ previewPhotoIndex === 0 &&
548
+ mergedStrings.secondPrompt ? (react_1.default.createElement(react_native_1.Text, { style: styles.confirmationPromptText }, mergedStrings.secondPrompt)) : null,
521
549
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
522
550
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
523
551
  react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.retake)),
524
- isBusinessMode && capturedPhotos.length === 1 && currentPhotoIndex === 1 ? (react_1.default.createElement(react_1.default.Fragment, null,
552
+ isBusinessMode &&
553
+ capturedPhotos.length === 1 &&
554
+ currentPhotoIndex === 1 &&
555
+ previewPhotoIndex === 0 ? (react_1.default.createElement(react_1.default.Fragment, null,
525
556
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleCaptureSecondPhoto, accessibilityLabel: mergedStrings.secondBtn, accessibilityRole: "button" },
526
557
  react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.secondBtn)),
527
558
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.skipButton], onPress: handleSkipSecondPhoto, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button" },
@@ -718,6 +749,14 @@ const styles = react_native_1.StyleSheet.create({
718
749
  width: '100%',
719
750
  height: '80%',
720
751
  },
752
+ confirmationPromptText: {
753
+ color: '#fff',
754
+ fontSize: 18,
755
+ fontWeight: '600',
756
+ textAlign: 'center',
757
+ paddingHorizontal: 32,
758
+ marginTop: 24,
759
+ },
721
760
  confirmationButtons: {
722
761
  flexDirection: 'row',
723
762
  justifyContent: 'center',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.114.0",
3
+ "version": "3.115.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -138,6 +138,7 @@ export interface FullDocScannerStrings {
138
138
  first?: string;
139
139
  second?: string;
140
140
  secondBtn?: string;
141
+ secondPrompt?: string;
141
142
  }
142
143
 
143
144
  export interface FullDocScannerProps {
@@ -182,16 +183,22 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
182
183
  const [rotationDegrees, setRotationDegrees] = useState(0);
183
184
  const [capturedPhotos, setCapturedPhotos] = useState<FullDocScannerResult[]>([]);
184
185
  const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
186
+ const [previewPhotoIndex, setPreviewPhotoIndex] = useState(0);
185
187
  const resolvedGridColor = gridColor ?? overlayColor;
186
188
  const docScannerRef = useRef<DocScannerHandle | null>(null);
187
189
  const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
188
190
  const captureInProgressRef = useRef(false);
189
191
  const rectangleCaptureTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
190
192
  const rectangleHintTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
193
+ const currentPhotoIndexRef = useRef(currentPhotoIndex);
191
194
 
192
195
  const isBusinessMode = type === 'business';
193
196
  const maxPhotos = isBusinessMode ? 2 : 1;
194
197
 
198
+ useEffect(() => {
199
+ currentPhotoIndexRef.current = currentPhotoIndex;
200
+ }, [currentPhotoIndex]);
201
+
195
202
  const mergedStrings = useMemo(
196
203
  () => ({
197
204
  captureHint: strings?.captureHint,
@@ -205,6 +212,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
205
212
  first: strings?.first ?? 'Front',
206
213
  second: strings?.second ?? 'Back',
207
214
  secondBtn: strings?.secondBtn ?? 'Capture Back Side?',
215
+ secondPrompt: strings?.secondPrompt ?? strings?.secondBtn ?? 'Capture Back Side?',
208
216
  }),
209
217
  [strings],
210
218
  );
@@ -261,6 +269,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
261
269
 
262
270
  setProcessing(false);
263
271
 
272
+ setPreviewPhotoIndex(currentPhotoIndexRef.current);
273
+
264
274
  // Show confirmation screen
265
275
  setCroppedImageData({
266
276
  path: croppedImage.path,
@@ -323,6 +333,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
323
333
  return;
324
334
  }
325
335
 
336
+ const previewIndex = currentPhotoIndexRef.current;
337
+ setPreviewPhotoIndex(previewIndex);
338
+
326
339
  const normalizedDoc = normalizeCapturedDocument(document);
327
340
 
328
341
  if (captureMode === 'no-grid') {
@@ -574,6 +587,21 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
574
587
 
575
588
  const handleRetake = useCallback(() => {
576
589
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
590
+
591
+ if (isBusinessMode) {
592
+ if (currentPhotoIndex === 1 && previewPhotoIndex === 0 && capturedPhotos.length === 1) {
593
+ console.log('[FullDocScanner] Retake detected on front preview after confirmation - reverting to front capture');
594
+ setCapturedPhotos([]);
595
+ setCurrentPhotoIndex(0);
596
+ setPreviewPhotoIndex(0);
597
+ } else if (previewPhotoIndex === 1 && capturedPhotos.length === 2) {
598
+ console.log('[FullDocScanner] Retake detected on back preview after final confirmation - removing back photo');
599
+ setCapturedPhotos(capturedPhotos.slice(0, 1));
600
+ setCurrentPhotoIndex(1);
601
+ setPreviewPhotoIndex(1);
602
+ }
603
+ }
604
+
577
605
  setCroppedImageData(null);
578
606
  setRotationDegrees(0);
579
607
  setProcessing(false);
@@ -593,7 +621,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
593
621
  if (docScannerRef.current?.reset) {
594
622
  docScannerRef.current.reset();
595
623
  }
596
- }, []);
624
+ }, [capturedPhotos, currentPhotoIndex, isBusinessMode, previewPhotoIndex]);
597
625
 
598
626
  const handleRectangleDetect = useCallback((event: RectangleDetectEvent) => {
599
627
  const stableCounter = event.stableCounter ?? 0;
@@ -661,7 +689,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
661
689
  {isBusinessMode && (
662
690
  <View style={styles.photoHeader}>
663
691
  <Text style={styles.photoHeaderText}>
664
- {currentPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second}
692
+ {previewPhotoIndex === 0 ? mergedStrings.first : mergedStrings.second}
665
693
  </Text>
666
694
  </View>
667
695
  )}
@@ -697,6 +725,13 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
697
725
  ]}
698
726
  resizeMode="contain"
699
727
  />
728
+ {isBusinessMode &&
729
+ capturedPhotos.length === 1 &&
730
+ currentPhotoIndex === 1 &&
731
+ previewPhotoIndex === 0 &&
732
+ mergedStrings.secondPrompt ? (
733
+ <Text style={styles.confirmationPromptText}>{mergedStrings.secondPrompt}</Text>
734
+ ) : null}
700
735
  <View style={styles.confirmationButtons}>
701
736
  <TouchableOpacity
702
737
  style={[styles.confirmButton, styles.retakeButton]}
@@ -708,7 +743,10 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
708
743
  </TouchableOpacity>
709
744
 
710
745
  {/* Business 모드이고 첫 번째 사진을 찍은 후: 뒷면 촬영 버튼 또는 확인 버튼 */}
711
- {isBusinessMode && capturedPhotos.length === 1 && currentPhotoIndex === 1 ? (
746
+ {isBusinessMode &&
747
+ capturedPhotos.length === 1 &&
748
+ currentPhotoIndex === 1 &&
749
+ previewPhotoIndex === 0 ? (
712
750
  <>
713
751
  <TouchableOpacity
714
752
  style={[styles.confirmButton, styles.confirmButtonPrimary]}
@@ -1008,6 +1046,14 @@ const styles = StyleSheet.create({
1008
1046
  width: '100%',
1009
1047
  height: '80%',
1010
1048
  },
1049
+ confirmationPromptText: {
1050
+ color: '#fff',
1051
+ fontSize: 18,
1052
+ fontWeight: '600',
1053
+ textAlign: 'center',
1054
+ paddingHorizontal: 32,
1055
+ marginTop: 24,
1056
+ },
1011
1057
  confirmationButtons: {
1012
1058
  flexDirection: 'row',
1013
1059
  justifyContent: 'center',