react-native-rectangle-doc-scanner 3.77.0 → 3.78.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.
@@ -43,6 +43,56 @@ const react_native_image_picker_1 = require("react-native-image-picker");
43
43
  const react_native_image_crop_picker_1 = __importDefault(require("react-native-image-crop-picker"));
44
44
  const DocScanner_1 = require("./DocScanner");
45
45
  const stripFileUri = (value) => value.replace(/^file:\/\//, '');
46
+ const CROPPER_TIMEOUT_MS = 8000;
47
+ const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
48
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
49
+ const runAfterInteractions = () => new Promise((resolve) => react_native_1.InteractionManager.runAfterInteractions(() => resolve()));
50
+ // Allow native pickers to finish their dismissal animations before presenting the cropper.
51
+ const waitForModalDismissal = async () => {
52
+ await delay(50);
53
+ await runAfterInteractions();
54
+ if (typeof requestAnimationFrame === 'function') {
55
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
56
+ }
57
+ await delay(180);
58
+ await runAfterInteractions();
59
+ };
60
+ // Guard the native cropper promise so we can recover if it never resolves.
61
+ async function withTimeout(factory) {
62
+ let timeoutId;
63
+ let finished = false;
64
+ const promise = factory()
65
+ .then((value) => {
66
+ finished = true;
67
+ return value;
68
+ })
69
+ .catch((error) => {
70
+ finished = true;
71
+ throw error;
72
+ });
73
+ const timeoutPromise = new Promise((_, reject) => {
74
+ timeoutId = setTimeout(() => {
75
+ if (!finished) {
76
+ const timeoutError = new Error(CROPPER_TIMEOUT_CODE);
77
+ timeoutError.code = CROPPER_TIMEOUT_CODE;
78
+ reject(timeoutError);
79
+ }
80
+ }, CROPPER_TIMEOUT_MS);
81
+ });
82
+ try {
83
+ return await Promise.race([promise, timeoutPromise]);
84
+ }
85
+ finally {
86
+ if (timeoutId) {
87
+ clearTimeout(timeoutId);
88
+ }
89
+ if (!finished) {
90
+ promise.catch(() => {
91
+ console.warn('[FullDocScanner] Cropper promise settled after timeout');
92
+ });
93
+ }
94
+ }
95
+ }
46
96
  const normalizeCapturedDocument = (document) => {
47
97
  const { origin: _origin, ...rest } = document;
48
98
  const normalizedPath = stripFileUri(document.initialPath ?? document.path);
@@ -81,7 +131,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
81
131
  react_native_1.Alert.alert('Document Scanner', fallbackMessage);
82
132
  }
83
133
  }, [onError]);
84
- const openCropper = (0, react_1.useCallback)(async (imagePath) => {
134
+ const openCropper = (0, react_1.useCallback)(async (imagePath, options) => {
85
135
  try {
86
136
  console.log('[FullDocScanner] openCropper called with path:', imagePath);
87
137
  setProcessing(true);
@@ -92,7 +142,11 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
92
142
  cleanPath = cleanPath.replace('file://', '');
93
143
  }
94
144
  console.log('[FullDocScanner] Clean path for cropper:', cleanPath);
95
- const croppedImage = await react_native_image_crop_picker_1.default.openCropper({
145
+ const shouldWaitForPickerDismissal = options?.waitForPickerDismissal ?? true;
146
+ if (shouldWaitForPickerDismissal) {
147
+ await waitForModalDismissal();
148
+ }
149
+ const croppedImage = await withTimeout(() => react_native_image_crop_picker_1.default.openCropper({
96
150
  path: cleanPath,
97
151
  mediaType: 'photo',
98
152
  width: cropWidth,
@@ -102,7 +156,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
102
156
  freeStyleCropEnabled: true,
103
157
  includeBase64: true,
104
158
  compressImageQuality: 0.9,
105
- });
159
+ }));
106
160
  console.log('[FullDocScanner] Cropper returned:', {
107
161
  path: croppedImage.path,
108
162
  hasBase64: !!croppedImage.data,
@@ -117,8 +171,14 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
117
171
  catch (error) {
118
172
  console.error('[FullDocScanner] openCropper error:', error);
119
173
  setProcessing(false);
174
+ const errorCode = error?.code;
120
175
  const errorMessage = error?.message || String(error);
121
- if (errorMessage === 'User cancelled image selection' ||
176
+ if (errorCode === CROPPER_TIMEOUT_CODE || errorMessage === CROPPER_TIMEOUT_CODE) {
177
+ console.error('[FullDocScanner] Cropper timed out waiting for presentation');
178
+ emitError(error instanceof Error ? error : new Error('Cropper timed out'), 'Failed to open crop editor. Please try again.');
179
+ }
180
+ else if (errorCode === 'E_PICKER_CANCELLED' ||
181
+ errorMessage === 'User cancelled image selection' ||
122
182
  errorMessage.includes('cancelled') ||
123
183
  errorMessage.includes('cancel')) {
124
184
  console.log('[FullDocScanner] User cancelled cropper');
@@ -148,7 +208,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
148
208
  const normalizedDoc = normalizeCapturedDocument(document);
149
209
  if (captureMode === 'no-grid') {
150
210
  console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
151
- await openCropper(normalizedDoc.path);
211
+ await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
152
212
  return;
153
213
  }
154
214
  if (normalizedDoc.croppedPath) {
@@ -159,7 +219,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
159
219
  return;
160
220
  }
161
221
  console.log('[FullDocScanner] Fallback to manual crop (no croppedPath available)');
162
- await openCropper(normalizedDoc.path);
222
+ await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
163
223
  }, [openCropper]);
164
224
  const triggerManualCapture = (0, react_1.useCallback)(() => {
165
225
  const scannerInstance = docScannerRef.current;
@@ -244,9 +304,6 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
244
304
  }
245
305
  const imageUri = result.assets[0].uri;
246
306
  console.log('[FullDocScanner] Gallery image selected:', imageUri);
247
- // Defer cropper presentation until picker dismissal finishes to avoid hierarchy errors
248
- await new Promise((resolve) => react_native_1.InteractionManager.runAfterInteractions(() => resolve()));
249
- await new Promise((resolve) => setTimeout(resolve, 350));
250
307
  await openCropper(imageUri);
251
308
  }
252
309
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.77.0",
3
+ "version": "3.78.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -22,6 +22,68 @@ import type {
22
22
 
23
23
  const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
24
24
 
25
+ const CROPPER_TIMEOUT_MS = 8000;
26
+ const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
27
+
28
+ const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
29
+
30
+ const runAfterInteractions = () =>
31
+ new Promise<void>((resolve) => InteractionManager.runAfterInteractions(() => resolve()));
32
+
33
+ // Allow native pickers to finish their dismissal animations before presenting the cropper.
34
+ const waitForModalDismissal = async () => {
35
+ await delay(50);
36
+ await runAfterInteractions();
37
+ if (typeof requestAnimationFrame === 'function') {
38
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
39
+ }
40
+ await delay(180);
41
+ await runAfterInteractions();
42
+ };
43
+
44
+ // Guard the native cropper promise so we can recover if it never resolves.
45
+ async function withTimeout<T>(factory: () => Promise<T>): Promise<T> {
46
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
47
+ let finished = false;
48
+
49
+ const promise = factory()
50
+ .then((value) => {
51
+ finished = true;
52
+ return value;
53
+ })
54
+ .catch((error) => {
55
+ finished = true;
56
+ throw error;
57
+ });
58
+
59
+ const timeoutPromise = new Promise<T>((_, reject) => {
60
+ timeoutId = setTimeout(() => {
61
+ if (!finished) {
62
+ const timeoutError = new Error(CROPPER_TIMEOUT_CODE);
63
+ (timeoutError as any).code = CROPPER_TIMEOUT_CODE;
64
+ reject(timeoutError);
65
+ }
66
+ }, CROPPER_TIMEOUT_MS);
67
+ });
68
+
69
+ try {
70
+ return await Promise.race([promise, timeoutPromise]);
71
+ } finally {
72
+ if (timeoutId) {
73
+ clearTimeout(timeoutId);
74
+ }
75
+ if (!finished) {
76
+ promise.catch(() => {
77
+ console.warn('[FullDocScanner] Cropper promise settled after timeout');
78
+ });
79
+ }
80
+ }
81
+ }
82
+
83
+ type OpenCropperOptions = {
84
+ waitForPickerDismissal?: boolean;
85
+ };
86
+
25
87
  const normalizeCapturedDocument = (document: DocScannerCapture): CapturedDocument => {
26
88
  const { origin: _origin, ...rest } = document;
27
89
  const normalizedPath = stripFileUri(document.initialPath ?? document.path);
@@ -120,7 +182,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
120
182
  );
121
183
 
122
184
  const openCropper = useCallback(
123
- async (imagePath: string) => {
185
+ async (imagePath: string, options?: OpenCropperOptions) => {
124
186
  try {
125
187
  console.log('[FullDocScanner] openCropper called with path:', imagePath);
126
188
  setProcessing(true);
@@ -133,17 +195,25 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
133
195
  }
134
196
  console.log('[FullDocScanner] Clean path for cropper:', cleanPath);
135
197
 
136
- const croppedImage = await ImageCropPicker.openCropper({
137
- path: cleanPath,
138
- mediaType: 'photo',
139
- width: cropWidth,
140
- height: cropHeight,
141
- cropping: true,
142
- cropperToolbarTitle: 'Crop Document',
143
- freeStyleCropEnabled: true,
144
- includeBase64: true,
145
- compressImageQuality: 0.9,
146
- });
198
+ const shouldWaitForPickerDismissal = options?.waitForPickerDismissal ?? true;
199
+
200
+ if (shouldWaitForPickerDismissal) {
201
+ await waitForModalDismissal();
202
+ }
203
+
204
+ const croppedImage = await withTimeout(() =>
205
+ ImageCropPicker.openCropper({
206
+ path: cleanPath,
207
+ mediaType: 'photo',
208
+ width: cropWidth,
209
+ height: cropHeight,
210
+ cropping: true,
211
+ cropperToolbarTitle: 'Crop Document',
212
+ freeStyleCropEnabled: true,
213
+ includeBase64: true,
214
+ compressImageQuality: 0.9,
215
+ }),
216
+ );
147
217
 
148
218
  console.log('[FullDocScanner] Cropper returned:', {
149
219
  path: croppedImage.path,
@@ -161,11 +231,21 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
161
231
  console.error('[FullDocScanner] openCropper error:', error);
162
232
  setProcessing(false);
163
233
 
234
+ const errorCode = (error as any)?.code;
164
235
  const errorMessage = (error as any)?.message || String(error);
165
236
 
166
- if (errorMessage === 'User cancelled image selection' ||
167
- errorMessage.includes('cancelled') ||
168
- errorMessage.includes('cancel')) {
237
+ if (errorCode === CROPPER_TIMEOUT_CODE || errorMessage === CROPPER_TIMEOUT_CODE) {
238
+ console.error('[FullDocScanner] Cropper timed out waiting for presentation');
239
+ emitError(
240
+ error instanceof Error ? error : new Error('Cropper timed out'),
241
+ 'Failed to open crop editor. Please try again.',
242
+ );
243
+ } else if (
244
+ errorCode === 'E_PICKER_CANCELLED' ||
245
+ errorMessage === 'User cancelled image selection' ||
246
+ errorMessage.includes('cancelled') ||
247
+ errorMessage.includes('cancel')
248
+ ) {
169
249
  console.log('[FullDocScanner] User cancelled cropper');
170
250
  } else {
171
251
  emitError(
@@ -204,7 +284,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
204
284
 
205
285
  if (captureMode === 'no-grid') {
206
286
  console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
207
- await openCropper(normalizedDoc.path);
287
+ await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
208
288
  return;
209
289
  }
210
290
 
@@ -217,7 +297,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
217
297
  }
218
298
 
219
299
  console.log('[FullDocScanner] Fallback to manual crop (no croppedPath available)');
220
- await openCropper(normalizedDoc.path);
300
+ await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
221
301
  },
222
302
  [openCropper],
223
303
  );
@@ -327,12 +407,6 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
327
407
  const imageUri = result.assets[0].uri;
328
408
  console.log('[FullDocScanner] Gallery image selected:', imageUri);
329
409
 
330
- // Defer cropper presentation until picker dismissal finishes to avoid hierarchy errors
331
- await new Promise<void>((resolve) =>
332
- InteractionManager.runAfterInteractions(() => resolve()),
333
- );
334
- await new Promise((resolve) => setTimeout(resolve, 350));
335
-
336
410
  await openCropper(imageUri);
337
411
  } catch (error) {
338
412
  console.error('[FullDocScanner] Gallery pick error:', error);
@@ -35,14 +35,18 @@ RCT_EXPORT_VIEW_PROPERTY(brightness, float)
35
35
  RCT_EXPORT_VIEW_PROPERTY(contrast, float)
36
36
 
37
37
  // Main capture method - accept reactTag when available (falls back to cached view)
38
- RCT_EXPORT_METHOD(capture:(nullable id)reactTag
38
+ RCT_EXPORT_METHOD(capture:(NSNumber * _Nullable)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
44
 
45
- if ([reactTag isKindOfClass:[NSNumber class]]) {
45
+ if ([reactTag isKindOfClass:[NSNull class]]) {
46
+ reactTag = nil;
47
+ }
48
+
49
+ if (reactTag) {
46
50
  NSNumber *resolvedTag = (NSNumber *)reactTag;
47
51
  UIView *view = [self.bridge.uiManager viewForReactTag:resolvedTag];
48
52
  if ([view isKindOfClass:[DocumentScannerView class]]) {
@@ -53,8 +57,6 @@ RCT_EXPORT_METHOD(capture:(nullable id)reactTag
53
57
  } else {
54
58
  NSLog(@"[RNPdfScannerManager] No view found for tag %@", resolvedTag);
55
59
  }
56
- } else if (reactTag) {
57
- NSLog(@"[RNPdfScannerManager] Unexpected reactTag type %@ - ignoring reactTag", NSStringFromClass([reactTag class]));
58
60
  }
59
61
 
60
62
  if (!targetView && self->_scannerView) {