react-native-rectangle-doc-scanner 3.76.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.
package/dist/DocScanner.js
CHANGED
|
@@ -43,6 +43,7 @@ const react_native_document_scanner_1 = __importDefault(require("react-native-do
|
|
|
43
43
|
const coordinate_1 = require("./utils/coordinate");
|
|
44
44
|
const overlay_1 = require("./utils/overlay");
|
|
45
45
|
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
46
|
+
const { RNPdfScannerManager } = react_native_1.NativeModules;
|
|
46
47
|
const normalizePoint = (point) => {
|
|
47
48
|
if (!point || !isFiniteNumber(point.x) || !isFiniteNumber(point.y)) {
|
|
48
49
|
return null;
|
|
@@ -155,7 +156,7 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
155
156
|
console.log('[DocScanner] capture() called');
|
|
156
157
|
captureOriginRef.current = 'manual';
|
|
157
158
|
const instance = scannerRef.current;
|
|
158
|
-
if (!instance || typeof instance.capture !== 'function') {
|
|
159
|
+
if (!instance || (typeof instance.capture !== 'function' && react_native_1.Platform.OS !== 'ios')) {
|
|
159
160
|
console.error('[DocScanner] Native instance not ready:', {
|
|
160
161
|
hasInstance: !!instance,
|
|
161
162
|
hasCaptureMethod: instance ? typeof instance.capture : 'no instance',
|
|
@@ -168,7 +169,47 @@ exports.DocScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAUL
|
|
|
168
169
|
captureOriginRef.current = 'auto';
|
|
169
170
|
return Promise.reject(new Error('capture_in_progress'));
|
|
170
171
|
}
|
|
171
|
-
|
|
172
|
+
const attemptNativeManagerCapture = () => {
|
|
173
|
+
if (react_native_1.Platform.OS !== 'ios') {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (!RNPdfScannerManager || typeof RNPdfScannerManager.capture !== 'function') {
|
|
177
|
+
console.warn('[DocScanner] RNPdfScannerManager.capture not available, falling back to instance method');
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const nodeHandle = (0, react_native_1.findNodeHandle)(instance);
|
|
181
|
+
if (!nodeHandle) {
|
|
182
|
+
console.error('[DocScanner] Unable to resolve native tag for scanner view');
|
|
183
|
+
return Promise.reject(new Error('scanner_view_not_ready'));
|
|
184
|
+
}
|
|
185
|
+
console.log('[DocScanner] Calling RNPdfScannerManager.capture with tag:', nodeHandle);
|
|
186
|
+
const managerResult = RNPdfScannerManager.capture(nodeHandle);
|
|
187
|
+
if (!managerResult || typeof managerResult.then !== 'function') {
|
|
188
|
+
console.warn('[DocScanner] RNPdfScannerManager.capture did not return a promise, falling back to instance method');
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return managerResult
|
|
192
|
+
.then((payload) => {
|
|
193
|
+
console.log('[DocScanner] RNPdfScannerManager promise resolved');
|
|
194
|
+
handlePictureTaken(payload);
|
|
195
|
+
return payload;
|
|
196
|
+
})
|
|
197
|
+
.catch((error) => {
|
|
198
|
+
console.error('[DocScanner] RNPdfScannerManager promise rejected:', error);
|
|
199
|
+
captureOriginRef.current = 'auto';
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const managerPromise = attemptNativeManagerCapture();
|
|
204
|
+
if (managerPromise) {
|
|
205
|
+
return managerPromise;
|
|
206
|
+
}
|
|
207
|
+
if (!instance || typeof instance.capture !== 'function') {
|
|
208
|
+
console.error('[DocScanner] capture() fallback not available on instance');
|
|
209
|
+
captureOriginRef.current = 'auto';
|
|
210
|
+
return Promise.reject(new Error('capture_not_supported'));
|
|
211
|
+
}
|
|
212
|
+
console.log('[DocScanner] Calling native capture method (legacy/event-based)...');
|
|
172
213
|
try {
|
|
173
214
|
const result = instance.capture();
|
|
174
215
|
console.log('[DocScanner] Native capture method called, result type:', typeof result, 'isPromise:', !!(result && typeof result.then === 'function'));
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
package/src/DocScanner.tsx
CHANGED
|
@@ -8,7 +8,14 @@ import React, {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
} from 'react';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Platform,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
TouchableOpacity,
|
|
15
|
+
View,
|
|
16
|
+
NativeModules,
|
|
17
|
+
findNodeHandle,
|
|
18
|
+
} from 'react-native';
|
|
12
19
|
import DocumentScanner from 'react-native-document-scanner';
|
|
13
20
|
import type { Rectangle as NativeRectangle, RectangleEventPayload } from 'react-native-document-scanner';
|
|
14
21
|
import { rectangleToQuad } from './utils/coordinate';
|
|
@@ -42,6 +49,8 @@ export type DocScannerCapture = {
|
|
|
42
49
|
const isFiniteNumber = (value: unknown): value is number =>
|
|
43
50
|
typeof value === 'number' && Number.isFinite(value);
|
|
44
51
|
|
|
52
|
+
const { RNPdfScannerManager } = NativeModules;
|
|
53
|
+
|
|
45
54
|
const normalizePoint = (point?: { x?: number; y?: number } | null): Point | null => {
|
|
46
55
|
if (!point || !isFiniteNumber(point.x) || !isFiniteNumber(point.y)) {
|
|
47
56
|
return null;
|
|
@@ -232,10 +241,10 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
232
241
|
captureOriginRef.current = 'manual';
|
|
233
242
|
const instance = scannerRef.current;
|
|
234
243
|
|
|
235
|
-
if (!instance || typeof instance.capture !== 'function') {
|
|
244
|
+
if (!instance || (typeof instance.capture !== 'function' && Platform.OS !== 'ios')) {
|
|
236
245
|
console.error('[DocScanner] Native instance not ready:', {
|
|
237
246
|
hasInstance: !!instance,
|
|
238
|
-
hasCaptureMethod: instance ? typeof instance.capture : 'no instance',
|
|
247
|
+
hasCaptureMethod: instance ? typeof (instance as any).capture : 'no instance',
|
|
239
248
|
});
|
|
240
249
|
captureOriginRef.current = 'auto';
|
|
241
250
|
return Promise.reject(new Error('DocumentScanner native instance is not ready'));
|
|
@@ -247,7 +256,57 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
247
256
|
return Promise.reject(new Error('capture_in_progress'));
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
|
|
259
|
+
const attemptNativeManagerCapture = () => {
|
|
260
|
+
if (Platform.OS !== 'ios') {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!RNPdfScannerManager || typeof RNPdfScannerManager.capture !== 'function') {
|
|
265
|
+
console.warn('[DocScanner] RNPdfScannerManager.capture not available, falling back to instance method');
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const nodeHandle = findNodeHandle(instance);
|
|
270
|
+
|
|
271
|
+
if (!nodeHandle) {
|
|
272
|
+
console.error('[DocScanner] Unable to resolve native tag for scanner view');
|
|
273
|
+
return Promise.reject(new Error('scanner_view_not_ready'));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log('[DocScanner] Calling RNPdfScannerManager.capture with tag:', nodeHandle);
|
|
277
|
+
|
|
278
|
+
const managerResult = RNPdfScannerManager.capture(nodeHandle);
|
|
279
|
+
|
|
280
|
+
if (!managerResult || typeof managerResult.then !== 'function') {
|
|
281
|
+
console.warn('[DocScanner] RNPdfScannerManager.capture did not return a promise, falling back to instance method');
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return managerResult
|
|
286
|
+
.then((payload: PictureEvent) => {
|
|
287
|
+
console.log('[DocScanner] RNPdfScannerManager promise resolved');
|
|
288
|
+
handlePictureTaken(payload);
|
|
289
|
+
return payload;
|
|
290
|
+
})
|
|
291
|
+
.catch((error: unknown) => {
|
|
292
|
+
console.error('[DocScanner] RNPdfScannerManager promise rejected:', error);
|
|
293
|
+
captureOriginRef.current = 'auto';
|
|
294
|
+
throw error;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const managerPromise = attemptNativeManagerCapture();
|
|
299
|
+
if (managerPromise) {
|
|
300
|
+
return managerPromise;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!instance || typeof instance.capture !== 'function') {
|
|
304
|
+
console.error('[DocScanner] capture() fallback not available on instance');
|
|
305
|
+
captureOriginRef.current = 'auto';
|
|
306
|
+
return Promise.reject(new Error('capture_not_supported'));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log('[DocScanner] Calling native capture method (legacy/event-based)...');
|
|
251
310
|
try {
|
|
252
311
|
const result = instance.capture();
|
|
253
312
|
console.log('[DocScanner] Native capture method called, result type:', typeof result, 'isPromise:', !!(result && typeof result.then === 'function'));
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 (
|
|
167
|
-
|
|
168
|
-
|
|
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:(
|
|
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:[
|
|
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) {
|