react-native-expo-cropper 1.2.46 → 1.2.48

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/README.MD CHANGED
@@ -32,6 +32,25 @@ EXPO SDK 54 -------------------------------------------------------------
32
32
  },
33
33
 
34
34
 
35
+ ## ✅ IMPORTANT (APK / production build)
36
+
37
+ Expo Go already contains native camera modules/permissions. In a **standalone APK** you must ensure the native config is present.
38
+
39
+ Add this to your `app.json` / `app.config.js` (example):
40
+
41
+ ```json
42
+ {
43
+ "expo": {
44
+ "plugins": ["expo-camera", "expo-image-picker", "expo-media-library"],
45
+ "android": {
46
+ "permissions": ["CAMERA"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Then rebuild the APK (EAS / prebuild). If the camera crashes only in release, it’s almost always missing permissions/plugin config or a device-specific camera processing issue.
53
+
35
54
  ------------------------------ DO THIS IN YOUR APP SCREEN CODE !!!!!!!!!! :-------------------------------
36
55
 
37
56
  import ImageCropper from 'react-native-expo-cropper/src/ImageCropper';
@@ -9,7 +9,7 @@ var _react = _interopRequireWildcard(require("react"));
9
9
  var _reactNative = require("react-native");
10
10
  var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
11
11
  var _expoCamera = require("expo-camera");
12
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t2 in e) "default" !== _t2 && {}.hasOwnProperty.call(e, _t2) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t2)) && (i.get || i.set) ? o(f, _t2, i) : f[_t2] = e[_t2]); return f; })(e, t); }
12
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t3 in e) "default" !== _t3 && {}.hasOwnProperty.call(e, _t3) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t3)) && (i.get || i.set) ? o(f, _t3, i) : f[_t3] = e[_t3]); return f; })(e, t); }
13
13
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
14
14
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
15
15
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
@@ -79,6 +79,34 @@ function CustomCamera(_ref) {
79
79
  }, _callee);
80
80
  }))();
81
81
  }, []);
82
+ var requestPermissionAgain = /*#__PURE__*/function () {
83
+ var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
84
+ var _yield$Camera$request2, status, _t;
85
+ return _regenerator().w(function (_context2) {
86
+ while (1) switch (_context2.p = _context2.n) {
87
+ case 0:
88
+ _context2.p = 0;
89
+ _context2.n = 1;
90
+ return _expoCamera.Camera.requestCameraPermissionsAsync();
91
+ case 1:
92
+ _yield$Camera$request2 = _context2.v;
93
+ status = _yield$Camera$request2.status;
94
+ setHasPermission(status === 'granted');
95
+ _context2.n = 3;
96
+ break;
97
+ case 2:
98
+ _context2.p = 2;
99
+ _t = _context2.v;
100
+ setHasPermission(false);
101
+ case 3:
102
+ return _context2.a(2);
103
+ }
104
+ }, _callee2, null, [[0, 2]]);
105
+ }));
106
+ return function requestPermissionAgain() {
107
+ return _ref3.apply(this, arguments);
108
+ };
109
+ }();
82
110
 
83
111
  // Helper function to wait for multiple render cycles (works on iOS)
84
112
  var waitForRender = function waitForRender() {
@@ -144,25 +172,32 @@ function CustomCamera(_ref) {
144
172
  }
145
173
  }, [cameraWrapperLayout]);
146
174
  var takePicture = /*#__PURE__*/function () {
147
- var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
148
- var captureOptions, photo, capturedAspectRatio, greenFrameCoords, _t;
149
- return _regenerator().w(function (_context2) {
150
- while (1) switch (_context2.p = _context2.n) {
175
+ var _ref4 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
176
+ var captureOptions, photo, capturedAspectRatio, greenFrameCoords, _t2;
177
+ return _regenerator().w(function (_context3) {
178
+ while (1) switch (_context3.p = _context3.n) {
151
179
  case 0:
180
+ if (hasPermission) {
181
+ _context3.n = 1;
182
+ break;
183
+ }
184
+ _reactNative.Alert.alert("Permission requise", "Veuillez autoriser l'accès à la caméra pour prendre une photo.");
185
+ return _context3.a(2);
186
+ case 1:
152
187
  if (!cameraRef.current) {
153
- _context2.n = 7;
188
+ _context3.n = 8;
154
189
  break;
155
190
  }
156
- _context2.p = 1;
191
+ _context3.p = 2;
157
192
  // Show loading after a delay (using setImmediate for iOS compatibility)
158
193
  waitForRender(5).then(function () {
159
194
  setLoadingBeforeCapture(true);
160
195
  });
161
196
 
162
197
  // Wait a bit before taking picture (works on iOS)
163
- _context2.n = 2;
198
+ _context3.n = 3;
164
199
  return waitForRender(2);
165
- case 2:
200
+ case 3:
166
201
  // ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
167
202
  // Platform-specific optimizations for best quality
168
203
  captureOptions = _objectSpread(_objectSpread({
@@ -172,7 +207,8 @@ function CustomCamera(_ref) {
172
207
  shutterSound: false,
173
208
  // ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
174
209
  // This ensures pixel-perfect quality and prevents premature resizing
175
- skipProcessing: true,
210
+ // NOTE: Some Android devices are unstable with skipProcessing; keep it off there.
211
+ skipProcessing: _reactNative.Platform.OS === 'ios',
176
212
  // Include EXIF metadata (orientation, camera settings, etc.)
177
213
  exif: true
178
214
  }, _reactNative.Platform.OS === 'ios' && {
@@ -190,16 +226,16 @@ function CustomCamera(_ref) {
190
226
  height: cameraWrapperLayout.height
191
227
  }
192
228
  });
193
- _context2.n = 3;
229
+ _context3.n = 4;
194
230
  return cameraRef.current.takePictureAsync(captureOptions);
195
- case 3:
196
- photo = _context2.v;
231
+ case 4:
232
+ photo = _context3.v;
197
233
  if (!(!photo.width || !photo.height || photo.width === 0 || photo.height === 0)) {
198
- _context2.n = 4;
234
+ _context3.n = 5;
199
235
  break;
200
236
  }
201
237
  throw new Error("Invalid photo dimensions received from camera");
202
- case 4:
238
+ case 5:
203
239
  capturedAspectRatio = photo.width / photo.height;
204
240
  console.log("✅ Photo captured with maximum quality:", {
205
241
  uri: photo.uri,
@@ -215,11 +251,11 @@ function CustomCamera(_ref) {
215
251
  // Fallback to recalculation if, for some reason, state is not yet set
216
252
  greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
217
253
  if (greenFrameCoords) {
218
- _context2.n = 5;
254
+ _context3.n = 6;
219
255
  break;
220
256
  }
221
257
  throw new Error("Green frame coordinates not available");
222
- case 5:
258
+ case 6:
223
259
  // ✅ Send photo URI and frame data to ImageCropper
224
260
  // The photo maintains its native resolution and aspect ratio
225
261
  // ImageCropper will handle display and cropping while preserving quality
@@ -232,26 +268,42 @@ function CustomCamera(_ref) {
232
268
  }
233
269
  });
234
270
  setLoadingBeforeCapture(false);
235
- _context2.n = 7;
271
+ _context3.n = 8;
236
272
  break;
237
- case 6:
238
- _context2.p = 6;
239
- _t = _context2.v;
240
- console.error("❌ Error capturing photo:", _t);
241
- setLoadingBeforeCapture(false);
242
- _reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
243
273
  case 7:
244
- return _context2.a(2);
274
+ _context3.p = 7;
275
+ _t2 = _context3.v;
276
+ console.error("❌ Error capturing photo:", _t2);
277
+ setLoadingBeforeCapture(false);
278
+ _reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t2.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
279
+ case 8:
280
+ return _context3.a(2);
245
281
  }
246
- }, _callee2, null, [[1, 6]]);
282
+ }, _callee3, null, [[2, 7]]);
247
283
  }));
248
284
  return function takePicture() {
249
- return _ref3.apply(this, arguments);
285
+ return _ref4.apply(this, arguments);
250
286
  };
251
287
  }();
252
288
  return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
253
289
  style: styles.outerContainer
254
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
290
+ }, hasPermission === null && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
291
+ style: styles.permissionContainer
292
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
293
+ size: "large",
294
+ color: PRIMARY_GREEN
295
+ }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
296
+ style: styles.permissionText
297
+ }, "Demande d'autorisation cam\xE9ra...")), hasPermission === false && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
298
+ style: styles.permissionContainer
299
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
300
+ style: styles.permissionText
301
+ }, "Autorisation cam\xE9ra refus\xE9e. Activez-la dans les param\xE8tres pour continuer."), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
302
+ style: styles.permissionButton,
303
+ onPress: requestPermissionAgain
304
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
305
+ style: styles.permissionButtonText
306
+ }, "R\xE9essayer"))), hasPermission === true && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
255
307
  style: [styles.cameraWrapper, {
256
308
  width: cameraPreviewWidth
257
309
  }],
@@ -279,10 +331,9 @@ function CustomCamera(_ref) {
279
331
  // This ensures preview matches what will be captured
280
332
  }), loadingBeforeCapture && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
281
333
  style: styles.loadingOverlay
282
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
283
- source: require('../src/assets/loadingCamera.gif'),
284
- style: styles.loadingGif,
285
- resizeMode: "contain"
334
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
335
+ size: "large",
336
+ color: PRIMARY_GREEN
286
337
  })), /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
287
338
  style: styles.touchBlocker,
288
339
  pointerEvents: "auto"
@@ -301,8 +352,8 @@ function CustomCamera(_ref) {
301
352
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
302
353
  style: styles.button,
303
354
  onPress: takePicture,
304
- disabled: !isReady || loadingBeforeCapture
305
- })));
355
+ disabled: !isReady || loadingBeforeCapture || !hasPermission
356
+ }))));
306
357
  }
307
358
  var PRIMARY_GREEN = '#198754';
308
359
  var DEEP_BLACK = '#0B0B0B';
@@ -314,6 +365,30 @@ var styles = _reactNative.StyleSheet.create({
314
365
  justifyContent: 'center',
315
366
  alignItems: 'center'
316
367
  },
368
+ permissionContainer: {
369
+ flex: 1,
370
+ width: '100%',
371
+ justifyContent: 'center',
372
+ alignItems: 'center',
373
+ paddingHorizontal: 24
374
+ },
375
+ permissionText: {
376
+ marginTop: 12,
377
+ fontSize: 14,
378
+ color: GLOW_WHITE,
379
+ textAlign: 'center'
380
+ },
381
+ permissionButton: {
382
+ marginTop: 16,
383
+ backgroundColor: PRIMARY_GREEN,
384
+ paddingHorizontal: 16,
385
+ paddingVertical: 10,
386
+ borderRadius: 8
387
+ },
388
+ permissionButtonText: {
389
+ color: 'white',
390
+ fontWeight: '600'
391
+ },
317
392
  cameraWrapper: {
318
393
  aspectRatio: 9 / 16,
319
394
  borderRadius: 30,
@@ -336,10 +411,6 @@ var styles = _reactNative.StyleSheet.create({
336
411
  justifyContent: 'center',
337
412
  alignItems: 'center'
338
413
  }),
339
- loadingGif: {
340
- width: 100,
341
- height: 100
342
- },
343
414
  touchBlocker: _objectSpread(_objectSpread({}, _reactNative.StyleSheet.absoluteFillObject), {}, {
344
415
  zIndex: 21,
345
416
  backgroundColor: 'transparent'
@@ -49,6 +49,15 @@ var ImageCropper = function ImageCropper(_ref) {
49
49
  var _useWindowDimensions = (0, _reactNative.useWindowDimensions)(),
50
50
  windowWidth = _useWindowDimensions.width;
51
51
  var cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
52
+ var guessExtensionFromUri = function guessExtensionFromUri(uri) {
53
+ if (!uri || typeof uri !== 'string') return null;
54
+ var match = uri.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
55
+ if (!match) return null;
56
+ var ext = match[1].toLowerCase();
57
+ // Keep only common image extensions
58
+ if (['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'].includes(ext)) return ext;
59
+ return null;
60
+ };
52
61
  var _useState = (0, _react.useState)(null),
53
62
  _useState2 = _slicedToArray(_useState, 2),
54
63
  image = _useState2[0],
@@ -1305,31 +1314,6 @@ var ImageCropper = function ImageCropper(_ref) {
1305
1314
  style: _ImageCropperStyles["default"].container
1306
1315
  }, showCustomCamera ? /*#__PURE__*/_react["default"].createElement(_CustomCamera["default"], {
1307
1316
  onPhotoCaptured: function onPhotoCaptured(uri, frameData) {
1308
- // ✅ Reset refs for new image so second (and later) photos don't use first image's layout (fixes white screen on some devices)
1309
- originalImageDimensions.current = {
1310
- width: 0,
1311
- height: 0
1312
- };
1313
- imageDisplayRect.current = {
1314
- x: 0,
1315
- y: 0,
1316
- width: 0,
1317
- height: 0
1318
- };
1319
- displayedImageLayout.current = {
1320
- x: 0,
1321
- y: 0,
1322
- width: 0,
1323
- height: 0
1324
- };
1325
- imageMeasure.current = {
1326
- x: 0,
1327
- y: 0,
1328
- width: 0,
1329
- height: 0
1330
- };
1331
- sourceImageUri.current = uri;
1332
-
1333
1317
  // ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
1334
1318
  if (frameData && frameData.greenFrame) {
1335
1319
  cameraFrameData.current = {
@@ -1425,7 +1409,6 @@ var ImageCropper = function ImageCropper(_ref) {
1425
1409
  return hasMovement;
1426
1410
  }
1427
1411
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
1428
- key: image,
1429
1412
  source: {
1430
1413
  uri: image
1431
1414
  },
@@ -1504,7 +1487,7 @@ var ImageCropper = function ImageCropper(_ref) {
1504
1487
  }, "Reset")), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1505
1488
  style: _ImageCropperStyles["default"].button,
1506
1489
  onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1507
- var _cameraFrameData$curr4, actualImageWidth, actualImageHeight, isCoverMode, captured, layout, contentRect, displayedWidth, displayedHeight, scale, coverOffsetX, coverOffsetY, scaledWidth, scaledHeight, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, name, _t2;
1490
+ var _cameraFrameData$curr4, actualImageWidth, actualImageHeight, isCoverMode, captured, layout, contentRect, displayedWidth, displayedHeight, scale, coverOffsetX, coverOffsetY, scaledWidth, scaledHeight, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, ext, safeExt, name, _t2;
1508
1491
  return _regenerator().w(function (_context2) {
1509
1492
  while (1) switch (_context2.p = _context2.n) {
1510
1493
  case 0:
@@ -1645,7 +1628,9 @@ var ImageCropper = function ImageCropper(_ref) {
1645
1628
  console.error("Error computing crop meta:", cropError);
1646
1629
  }
1647
1630
  }
1648
- name = "IMAGE XTK".concat(Date.now());
1631
+ ext = guessExtensionFromUri(originalUri) || 'jpg';
1632
+ safeExt = ext === 'jpeg' ? 'jpg' : ext;
1633
+ name = "IMAGE_XTK_".concat(Date.now(), ".").concat(safeExt);
1649
1634
  if (onConfirm) {
1650
1635
  onConfirm(originalUri, name, cropMeta);
1651
1636
  }
@@ -1698,10 +1683,9 @@ var ImageCropper = function ImageCropper(_ref) {
1698
1683
  animationType: "fade"
1699
1684
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1700
1685
  style: _ImageCropperStyles["default"].loadingOverlay
1701
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
1702
- source: require('../src/assets/loadingCamera.gif'),
1703
- style: _ImageCropperStyles["default"].loadingGif,
1704
- resizeMode: "contain"
1686
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
1687
+ size: "large",
1688
+ color: PRIMARY_GREEN
1705
1689
  }))));
1706
1690
  };
1707
1691
  var _default = exports["default"] = ImageCropper;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-expo-cropper",
3
- "version": "1.2.46",
3
+ "version": "1.2.48",
4
4
  "description": "Recadrage polygonal d'images.",
5
5
  "main": "index.js",
6
6
  "author": "PCS AGRI",
@@ -47,7 +47,7 @@
47
47
  "homepage": "https://github.com/pcsagri/react-native-expo-cropper#readme",
48
48
  "scripts": {
49
49
  "test": "echo \"Error: no test specified\" && exit 1",
50
- "build": "npx --package=@babel/cli babel src --out-dir dist"
50
+ "build": "node ./node_modules/@babel/cli/bin/babel.js src --out-dir dist"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@babel/cli": "^7.28.3",
@@ -6,8 +6,8 @@ import {
6
6
  TouchableOpacity,
7
7
  Alert,
8
8
  SafeAreaView,
9
- Image,
10
9
  Platform,
10
+ ActivityIndicator,
11
11
  useWindowDimensions,
12
12
  } from 'react-native';
13
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -35,6 +35,15 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
35
35
  })();
36
36
  }, []);
37
37
 
38
+ const requestPermissionAgain = async () => {
39
+ try {
40
+ const { status } = await Camera.requestCameraPermissionsAsync();
41
+ setHasPermission(status === 'granted');
42
+ } catch (e) {
43
+ setHasPermission(false);
44
+ }
45
+ };
46
+
38
47
  // Helper function to wait for multiple render cycles (works on iOS)
39
48
  const waitForRender = (cycles = 5) => {
40
49
  return new Promise((resolve) => {
@@ -102,6 +111,14 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
102
111
  }, [cameraWrapperLayout]);
103
112
 
104
113
  const takePicture = async () => {
114
+ if (!hasPermission) {
115
+ Alert.alert(
116
+ "Permission requise",
117
+ "Veuillez autoriser l'accès à la caméra pour prendre une photo."
118
+ );
119
+ return;
120
+ }
121
+
105
122
  if (cameraRef.current) {
106
123
  try {
107
124
  // Show loading after a delay (using setImmediate for iOS compatibility)
@@ -123,7 +140,8 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
123
140
 
124
141
  // ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
125
142
  // This ensures pixel-perfect quality and prevents premature resizing
126
- skipProcessing: true,
143
+ // NOTE: Some Android devices are unstable with skipProcessing; keep it off there.
144
+ skipProcessing: Platform.OS === 'ios',
127
145
 
128
146
  // Include EXIF metadata (orientation, camera settings, etc.)
129
147
  exif: true,
@@ -198,70 +216,88 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
198
216
 
199
217
  return (
200
218
  <SafeAreaView style={styles.outerContainer}>
201
- <View
202
- style={[styles.cameraWrapper, { width: cameraPreviewWidth }]}
203
- ref={cameraWrapperRef}
204
- onLayout={(e) => {
205
- const layout = e.nativeEvent.layout;
206
- setCameraWrapperLayout({
207
- width: layout.width,
208
- height: layout.height,
209
- x: layout.x,
210
- y: layout.y
211
- });
212
- console.log("Camera wrapper layout updated:", layout);
213
- }}
214
- >
215
- <CameraView
216
- style={styles.camera}
217
- facing="back"
218
- ref={cameraRef}
219
- onCameraReady={() => {
220
- setIsReady(true);
221
- console.log("✅ Camera ready - Maximum quality capture enabled");
222
- }}
223
- // ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
224
- // The wrapper constrains the view, but capture uses full sensor resolution
225
- // This ensures preview matches what will be captured
226
- />
227
-
228
- {/* Loading overlay */}
229
- {loadingBeforeCapture && (
230
- <>
231
- <View style={styles.loadingOverlay}>
232
- <Image
233
- source={require('../src/assets/loadingCamera.gif')}
234
- style={styles.loadingGif}
235
- resizeMode="contain"
236
- />
237
- </View>
238
- <View style={styles.touchBlocker} pointerEvents="auto" />
239
- </>
240
- )}
241
-
242
- {/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
243
- {greenFrame && (
219
+ {hasPermission === null && (
220
+ <View style={styles.permissionContainer}>
221
+ <ActivityIndicator size="large" color={PRIMARY_GREEN} />
222
+ <Text style={styles.permissionText}>Demande d'autorisation caméra...</Text>
223
+ </View>
224
+ )}
225
+
226
+ {hasPermission === false && (
227
+ <View style={styles.permissionContainer}>
228
+ <Text style={styles.permissionText}>
229
+ Autorisation caméra refusée. Activez-la dans les paramètres pour continuer.
230
+ </Text>
231
+ <TouchableOpacity style={styles.permissionButton} onPress={requestPermissionAgain}>
232
+ <Text style={styles.permissionButtonText}>Réessayer</Text>
233
+ </TouchableOpacity>
234
+ </View>
235
+ )}
236
+
237
+ {hasPermission === true && (
238
+ <>
244
239
  <View
245
- style={[
246
- styles.scanFrame,
247
- {
248
- left: greenFrame.x,
249
- top: greenFrame.y,
250
- width: greenFrame.width,
251
- height: greenFrame.height,
252
- },
253
- ]}
254
- />
255
- )}
256
- </View>
257
-
258
- <View style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
259
- <TouchableOpacity
260
- style={styles.button}
261
- onPress={takePicture}
262
- disabled={!isReady || loadingBeforeCapture}
263
- />
264
- </View>
240
+ style={[styles.cameraWrapper, { width: cameraPreviewWidth }]}
241
+ ref={cameraWrapperRef}
242
+ onLayout={(e) => {
243
+ const layout = e.nativeEvent.layout;
244
+ setCameraWrapperLayout({
245
+ width: layout.width,
246
+ height: layout.height,
247
+ x: layout.x,
248
+ y: layout.y
249
+ });
250
+ console.log("Camera wrapper layout updated:", layout);
251
+ }}
252
+ >
253
+ <CameraView
254
+ style={styles.camera}
255
+ facing="back"
256
+ ref={cameraRef}
257
+ onCameraReady={() => {
258
+ setIsReady(true);
259
+ console.log("✅ Camera ready - Maximum quality capture enabled");
260
+ }}
261
+ // ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
262
+ // The wrapper constrains the view, but capture uses full sensor resolution
263
+ // This ensures preview matches what will be captured
264
+ />
265
+
266
+ {/* Loading overlay */}
267
+ {loadingBeforeCapture && (
268
+ <>
269
+ <View style={styles.loadingOverlay}>
270
+ <ActivityIndicator size="large" color={PRIMARY_GREEN} />
271
+ </View>
272
+ <View style={styles.touchBlocker} pointerEvents="auto" />
273
+ </>
274
+ )}
275
+
276
+ {/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
277
+ {greenFrame && (
278
+ <View
279
+ style={[
280
+ styles.scanFrame,
281
+ {
282
+ left: greenFrame.x,
283
+ top: greenFrame.y,
284
+ width: greenFrame.width,
285
+ height: greenFrame.height,
286
+ },
287
+ ]}
288
+ />
289
+ )}
290
+ </View>
291
+
292
+ <View style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
293
+ <TouchableOpacity
294
+ style={styles.button}
295
+ onPress={takePicture}
296
+ disabled={!isReady || loadingBeforeCapture || !hasPermission}
297
+ />
298
+ </View>
299
+ </>
300
+ )}
265
301
  </SafeAreaView>
266
302
  );
267
303
  }
@@ -277,6 +313,30 @@ const styles = StyleSheet.create({
277
313
  justifyContent: 'center',
278
314
  alignItems: 'center',
279
315
  },
316
+ permissionContainer: {
317
+ flex: 1,
318
+ width: '100%',
319
+ justifyContent: 'center',
320
+ alignItems: 'center',
321
+ paddingHorizontal: 24,
322
+ },
323
+ permissionText: {
324
+ marginTop: 12,
325
+ fontSize: 14,
326
+ color: GLOW_WHITE,
327
+ textAlign: 'center',
328
+ },
329
+ permissionButton: {
330
+ marginTop: 16,
331
+ backgroundColor: PRIMARY_GREEN,
332
+ paddingHorizontal: 16,
333
+ paddingVertical: 10,
334
+ borderRadius: 8,
335
+ },
336
+ permissionButtonText: {
337
+ color: 'white',
338
+ fontWeight: '600',
339
+ },
280
340
  cameraWrapper: {
281
341
  aspectRatio: 9 / 16,
282
342
  borderRadius: 30,
@@ -302,10 +362,6 @@ const styles = StyleSheet.create({
302
362
  justifyContent: 'center',
303
363
  alignItems: 'center',
304
364
  },
305
- loadingGif: {
306
- width: 100,
307
- height: 100,
308
- },
309
365
  touchBlocker: {
310
366
  ...StyleSheet.absoluteFillObject,
311
367
  zIndex: 21,
@@ -17,6 +17,16 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
17
17
  const { width: windowWidth } = useWindowDimensions();
18
18
  const cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
19
19
 
20
+ const guessExtensionFromUri = (uri) => {
21
+ if (!uri || typeof uri !== 'string') return null;
22
+ const match = uri.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
23
+ if (!match) return null;
24
+ const ext = match[1].toLowerCase();
25
+ // Keep only common image extensions
26
+ if (['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'].includes(ext)) return ext;
27
+ return null;
28
+ };
29
+
20
30
  const [image, setImage] = useState(null);
21
31
  const [points, setPoints] = useState([]);
22
32
  const [showResult, setShowResult] = useState(false);
@@ -1100,13 +1110,6 @@ const rotatePreviewImage = async (degrees) => {
1100
1110
  {showCustomCamera ? (
1101
1111
  <CustomCamera
1102
1112
  onPhotoCaptured={(uri, frameData) => {
1103
- // ✅ Reset refs for new image so second (and later) photos don't use first image's layout (fixes white screen on some devices)
1104
- originalImageDimensions.current = { width: 0, height: 0 };
1105
- imageDisplayRect.current = { x: 0, y: 0, width: 0, height: 0 };
1106
- displayedImageLayout.current = { x: 0, y: 0, width: 0, height: 0 };
1107
- imageMeasure.current = { x: 0, y: 0, width: 0, height: 0 };
1108
- sourceImageUri.current = uri;
1109
-
1110
1113
  // ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
1111
1114
  if (frameData && frameData.greenFrame) {
1112
1115
  cameraFrameData.current = {
@@ -1204,7 +1207,6 @@ const rotatePreviewImage = async (degrees) => {
1204
1207
  }}
1205
1208
  >
1206
1209
  <Image
1207
- key={image}
1208
1210
  source={{ uri: image }}
1209
1211
  style={styles.image}
1210
1212
  resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
@@ -1382,7 +1384,9 @@ const rotatePreviewImage = async (degrees) => {
1382
1384
  }
1383
1385
  }
1384
1386
 
1385
- const name = `IMAGE XTK${Date.now()}`;
1387
+ const ext = guessExtensionFromUri(originalUri) || 'jpg';
1388
+ const safeExt = ext === 'jpeg' ? 'jpg' : ext;
1389
+ const name = `IMAGE_XTK_${Date.now()}.${safeExt}`;
1386
1390
  if (onConfirm) {
1387
1391
  onConfirm(originalUri, name, cropMeta);
1388
1392
  }
@@ -1439,14 +1443,10 @@ const rotatePreviewImage = async (degrees) => {
1439
1443
  )}
1440
1444
 
1441
1445
  <Modal visible={isLoading} transparent animationType="fade">
1442
- <View style={styles.loadingOverlay}>
1443
- <Image
1444
- source={require('../src/assets/loadingCamera.gif')}
1445
- style={styles.loadingGif}
1446
- resizeMode="contain"
1447
- />
1448
- </View>
1449
- </Modal>
1446
+ <View style={styles.loadingOverlay}>
1447
+ <ActivityIndicator size="large" color={PRIMARY_GREEN} />
1448
+ </View>
1449
+ </Modal>
1450
1450
  </View>
1451
1451
  );
1452
1452
  };