@widergy/mobile-ui 1.50.1 → 2.0.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/components/Carousel/components/CarouselContainer/index.js +5 -3
  3. package/lib/components/FilePicker/constants.js +1 -4
  4. package/lib/components/FilePicker/index.js +33 -17
  5. package/lib/components/ImagePicker/ModalSelectionOption/index.js +14 -2
  6. package/lib/components/ImagePicker/index.js +5 -1
  7. package/lib/components/ImagePicker/layout.js +65 -5
  8. package/lib/components/MultipleFilePicker/index.js +275 -35
  9. package/lib/components/MultipleFilePicker/propTypes.js +16 -1
  10. package/lib/components/MultipleFilePicker/utils.js +44 -13
  11. package/lib/components/Overlay/index.js +8 -3
  12. package/lib/components/PhotoAlbum/index.js +28 -4
  13. package/lib/components/RateChart/components/RateStagesGraph/components/Bars/index.js +5 -4
  14. package/lib/components/UTCheckBox/index.js +3 -15
  15. package/lib/components/UTCheckBox/theme.js +4 -18
  16. package/lib/components/UTCheckList/index.js +11 -7
  17. package/lib/components/UTDetailDrawer/index.js +4 -4
  18. package/lib/components/UTHeader/index.js +17 -9
  19. package/lib/components/UTIcon/index.js +0 -1
  20. package/lib/components/UTIcon/utils.js +3 -3
  21. package/lib/components/UTModal/index.js +4 -3
  22. package/lib/components/UTModal/styles.js +1 -2
  23. package/lib/components/UTRating/index.js +4 -3
  24. package/lib/components/UTRating/styles.js +3 -1
  25. package/lib/components/UTTabs/index.js +14 -6
  26. package/lib/components/UTTabs/styles.js +51 -19
  27. package/lib/components/UTTopbar/index.js +11 -8
  28. package/lib/components/UTTracker/components/Step/styles.js +8 -4
  29. package/lib/components/UTWorkflowContainer/versions/V1/components/BottomActions/components/ActionButton/index.js +4 -2
  30. package/lib/components/UTWorkflowContainer/versions/V1/components/BottomActions/index.js +15 -2
  31. package/lib/components/UTWorkflowContainer/versions/V1/index.js +19 -4
  32. package/lib/constants/testIds.js +30 -7
  33. package/lib/reactotronConfig.js +36 -3
  34. package/lib/utils/fileUtils.js/index.js +15 -10
  35. package/package.json +12 -10
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ # [2.0.0](https://github.com/widergy/mobile-ui/compare/v1.51.0...v2.0.0) (2025-09-22)
2
+
3
+
4
+ * feat!: [OUG-8855] update sdk (#453) ([ac9d0d3](https://github.com/widergy/mobile-ui/commit/ac9d0d3c5033f1bce8641b30f7afbdd15ae9857b)), closes [#453](https://github.com/widergy/mobile-ui/issues/453)
5
+
6
+
7
+ ### BREAKING CHANGES
8
+
9
+ * updates core dependencies
10
+
11
+ # [1.51.0](https://github.com/widergy/mobile-ui/compare/v1.50.1...v1.51.0) (2025-09-18)
12
+
13
+
14
+ ### Features
15
+
16
+ * [AUTO-47] Testids for workflowContainer components ([#452](https://github.com/widergy/mobile-ui/issues/452)) ([1fc2bb5](https://github.com/widergy/mobile-ui/commit/1fc2bb5893380f26186e0d089832b88668b91182))
17
+
1
18
  ## [1.50.1](https://github.com/widergy/mobile-ui/compare/v1.50.0...v1.50.1) (2025-09-18)
2
19
 
3
20
 
@@ -20,9 +20,11 @@ const CarouselContainer = ({ style, ...props }, ref) => {
20
20
  ]}
21
21
  >
22
22
  {single ? (
23
- <CarouselItem {...itemProps} single>
24
- {items.map(renderItem)}
25
- </CarouselItem>
23
+ items.map((item, i) => (
24
+ <CarouselItem key={`item-${i + 1}`} {...itemProps} single>
25
+ {renderItem(item)}
26
+ </CarouselItem>
27
+ ))
26
28
  ) : (
27
29
  <CarouselComponent ref={ref} {...props} />
28
30
  )}
@@ -1,10 +1,7 @@
1
- // eslint-disable-next-line import/no-unresolved
2
- import DocumentPicker from 'react-native-document-picker';
3
-
4
1
  import { MEGABYTE } from '../../utils/fileUtils.js';
5
2
 
6
3
  export const UPLOAD_ICON = 'file-upload';
7
4
 
8
- export const DEFAULT_ALLOWED_TYPES = [DocumentPicker.types.allFiles];
5
+ export const DEFAULT_ALLOWED_TYPES = ['*/*'];
9
6
 
10
7
  export const DEFAULT_MAX_SIZE = 10 * MEGABYTE;
@@ -1,11 +1,11 @@
1
1
  import React, { Component } from 'react';
2
2
  import { View } from 'react-native';
3
- // eslint-disable-next-line import/no-unresolved
4
- import DocumentPicker from 'react-native-document-picker';
5
3
  import { isEmpty } from '@widergy/web-utils/lib/array';
4
+ import * as ExpoDocumentPicker from 'expo-document-picker';
6
5
 
7
6
  import Picker from '../Picker';
8
7
  import { retrieveFile, isImageByUri, blobToFile } from '../../utils/fileUtils.js';
8
+ import { getMimeType } from '../MultipleFilePicker/utils';
9
9
 
10
10
  import { UPLOAD_ICON, DEFAULT_ALLOWED_TYPES, DEFAULT_MAX_SIZE } from './constants';
11
11
  import filePickerPropTypes from './propTypes';
@@ -21,23 +21,46 @@ class FilePicker extends Component {
21
21
 
22
22
  handleShowPicker = async () => {
23
23
  const {
24
- allowedTypes,
24
+ allowedTypes = DEFAULT_ALLOWED_TYPES,
25
25
  onMaxSizeError,
26
26
  onError,
27
27
  onChange,
28
- maxFileByteSize,
28
+ maxFileByteSize = DEFAULT_MAX_SIZE,
29
29
  avoidRetrieveFile,
30
30
  fileTypeError,
31
31
  allowedPDFUploadSizes,
32
32
  pdfFormatError
33
33
  } = this.props;
34
34
  try {
35
- const documents = await DocumentPicker.pick({
36
- type: allowedTypes && !onlyPDFAllowed(allowedTypes) ? allowedTypes : DocumentPicker.types.allFiles
35
+ const normalizeTypes = types => {
36
+ if (!types || types.length === 0) return DEFAULT_ALLOWED_TYPES;
37
+ const hasMime = types.some(t => typeof t === 'string' && t.includes('/'));
38
+ if (hasMime) return types;
39
+ const mapped = types.map(getMimeType).filter(Boolean);
40
+ return mapped.length ? mapped : DEFAULT_ALLOWED_TYPES;
41
+ };
42
+
43
+ const pickTypes = !onlyPDFAllowed(allowedTypes) ? normalizeTypes(allowedTypes) : DEFAULT_ALLOWED_TYPES;
44
+
45
+ const result = await ExpoDocumentPicker.getDocumentAsync({
46
+ multiple: false,
47
+ type: pickTypes
37
48
  });
38
- const document = documents[0];
39
- const isPDF = document.type.includes('pdf');
40
- const isImage = document.type.includes('image');
49
+
50
+ if (result.canceled) {
51
+ return;
52
+ }
53
+
54
+ const asset = (result.assets && result.assets[0]) || result;
55
+ const document = {
56
+ uri: asset.uri,
57
+ type: asset.mimeType || asset.type || 'application/octet-stream',
58
+ size: asset.size,
59
+ name: asset.name || 'document'
60
+ };
61
+
62
+ const isPDF = (document.type || '').includes('pdf');
63
+ const isImage = (document.type || '').includes('image');
41
64
  if (onlyPDFAllowed(allowedTypes) && !isPDF) {
42
65
  throw new Error(fileTypeError || 'El tipo de archivo debe ser PDF.');
43
66
  }
@@ -66,9 +89,7 @@ class FilePicker extends Component {
66
89
  }
67
90
  this.setState({ fileName: document.name });
68
91
  } catch (err) {
69
- if (!DocumentPicker.isCancel(err)) {
70
- onError(err.message);
71
- }
92
+ onError(err.message);
72
93
  }
73
94
  };
74
95
 
@@ -102,11 +123,6 @@ class FilePicker extends Component {
102
123
  }
103
124
  }
104
125
 
105
- FilePicker.defaultProps = {
106
- allowedTypes: DEFAULT_ALLOWED_TYPES,
107
- maxFileByteSize: DEFAULT_MAX_SIZE
108
- };
109
-
110
126
  FilePicker.propTypes = filePickerPropTypes;
111
127
 
112
128
  export default FilePicker;
@@ -29,10 +29,22 @@ const ModalSelectionOption = ({
29
29
  {title}
30
30
  </UTLabel>
31
31
  <View style={styles.container}>
32
- <UTButton onPress={() => openOptionSelected(true)} style={{ root: styles.button }} variant="text">
32
+ <UTButton
33
+ onPress={() => {
34
+ openOptionSelected(true);
35
+ }}
36
+ style={{ root: styles.button }}
37
+ variant="text"
38
+ >
33
39
  {takePhotoButtonTitle}
34
40
  </UTButton>
35
- <UTButton onPress={() => openOptionSelected(false)} style={{ root: styles.button }} variant="text">
41
+ <UTButton
42
+ onPress={() => {
43
+ openOptionSelected(false);
44
+ }}
45
+ style={{ root: styles.button }}
46
+ variant="text"
47
+ >
36
48
  {chooseFromLibraryButtonTitle}
37
49
  </UTButton>
38
50
  </View>
@@ -52,7 +52,11 @@ const ImagePickerComponent = ({
52
52
  return;
53
53
  }
54
54
  const { assets } = response;
55
- const item = assets[0];
55
+ const item = assets && assets.length > 0 ? assets[0] : null;
56
+ if (!item) {
57
+ onError('No se pudo obtener el archivo');
58
+ return;
59
+ }
56
60
  const file = !avoidRetrieveFile && (await retrieveFile(item.uri, item.type));
57
61
  if (!avoidRetrieveFile && !file) {
58
62
  onError(response.errorCode);
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
3
3
  import { View, Image, TouchableWithoutFeedback, PermissionsAndroid } from 'react-native';
4
4
  // eslint-disable-next-line import/no-unresolved
5
5
  import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
6
+ import * as ExpoImagePicker from 'expo-image-picker';
6
7
 
7
8
  import { IS_IOS } from '../../utils/platformUtils/constants';
8
9
  import Picker from '../Picker';
@@ -39,6 +40,7 @@ const ImagePicker = ({
39
40
  const [openModalSelection, setOpenModalSelection] = useState(false);
40
41
  const visibleModalSelection = () => setOpenModalSelection(true);
41
42
  const closeModalSelection = () => setOpenModalSelection(false);
43
+
42
44
  const requestPermissionCamera = async () => {
43
45
  try {
44
46
  const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
@@ -52,11 +54,69 @@ const ImagePicker = ({
52
54
  };
53
55
  const openOptionSelected = async selected => {
54
56
  closeModalSelection();
55
- return (
56
- editable &&
57
- onShowImagePicker(
58
- selected ? (IS_IOS ? launchCamera : await requestPermissionCamera()) : launchImageLibrary
59
- )
57
+ if (editable === false) return undefined;
58
+ if (typeof onShowImagePicker !== 'function') return undefined;
59
+ if (
60
+ !ExpoImagePicker ||
61
+ (!ExpoImagePicker.launchImageLibraryAsync && !ExpoImagePicker.launchCameraAsync)
62
+ ) {
63
+ // eslint-disable-next-line no-console
64
+ console.error('No se encontró un ExpoImagePicker válido. instala "expo-image-picker".');
65
+ return () => {};
66
+ }
67
+ if (ExpoImagePicker?.launchImageLibraryAsync || ExpoImagePicker?.launchCameraAsync) {
68
+ if (selected) {
69
+ const expoCameraAdapter = async (options, cb) => {
70
+ try {
71
+ const perm = await ExpoImagePicker.requestCameraPermissionsAsync?.();
72
+ if (!perm || perm.status !== 'granted') {
73
+ cb?.({ didCancel: true, errorCode: 'Son necesarios permisos para utilizar la cámara' });
74
+ return;
75
+ }
76
+ const mediaTypes = ExpoImagePicker.MediaType ? [ExpoImagePicker.MediaType.image] : undefined;
77
+ const result = await ExpoImagePicker.launchCameraAsync({
78
+ mediaTypes,
79
+ allowsMultipleSelection: false,
80
+ quality: 1
81
+ });
82
+ cb?.({ assets: result?.assets || [], didCancel: result?.canceled });
83
+ } catch (e) {
84
+ cb?.({ didCancel: true, errorCode: String(e) });
85
+ }
86
+ };
87
+ return onShowImagePicker(expoCameraAdapter);
88
+ }
89
+ const expoGalleryAdapter = async (options, cb) => {
90
+ try {
91
+ if (
92
+ !ExpoImagePicker ||
93
+ (!ExpoImagePicker.launchImageLibraryAsync && !ExpoImagePicker.launchCameraAsync)
94
+ ) {
95
+ // eslint-disable-next-line no-console
96
+ console.error('No se encontró un ExpoImagePicker válido. instala "expo-image-picker".');
97
+ return;
98
+ }
99
+ const perm = await ExpoImagePicker.requestMediaLibraryPermissionsAsync?.();
100
+ if (!perm || perm.status !== 'granted') {
101
+ cb?.({ didCancel: true, errorCode: 'Son necesarios permisos para acceder a tus fotos' });
102
+ return;
103
+ }
104
+ const mediaTypes = ExpoImagePicker.MediaType ? [ExpoImagePicker.MediaType.image] : undefined;
105
+ const result = await ExpoImagePicker.launchImageLibraryAsync({
106
+ mediaTypes,
107
+ allowsMultipleSelection: false,
108
+ quality: 1
109
+ });
110
+ cb?.({ assets: result?.assets || [], didCancel: result?.canceled });
111
+ } catch (e) {
112
+ cb?.({ didCancel: true, errorCode: String(e) });
113
+ }
114
+ };
115
+ return onShowImagePicker(expoGalleryAdapter);
116
+ }
117
+
118
+ return onShowImagePicker(
119
+ selected ? (IS_IOS ? launchCamera : await requestPermissionCamera()) : launchImageLibrary
60
120
  );
61
121
  };
62
122
 
@@ -1,20 +1,23 @@
1
- import { isArray, isEmpty } from 'lodash';
1
+ /* eslint-disable no-undef */
2
+ import isEmpty from 'lodash/isEmpty';
3
+ import isArray from 'lodash/isArray';
2
4
  // eslint-disable-next-line import/no-unresolved
3
5
  import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
4
6
  // eslint-disable-next-line react-native/split-platform-components
5
- import { View, PermissionsAndroid } from 'react-native';
6
- // eslint-disable-next-line import/no-unresolved
7
- import DocumentPicker from 'react-native-document-picker';
8
- import React, { useEffect, useState } from 'react';
7
+ import React, { View, PermissionsAndroid, Alert } from 'react-native';
8
+ import { useEffect, useState } from 'react';
9
+ import * as ExpoImagePicker from 'expo-image-picker';
10
+ import * as ExpoDocumentPicker from 'expo-document-picker';
9
11
 
10
12
  import { IS_IOS } from '../../utils/platformUtils/constants';
11
13
  import { retrieveFile, blobToFile } from '../../utils/fileUtils.js';
12
14
  import UTBottomSheet from '../UTBottomSheet';
13
15
  import UTButton from '../UTButton';
16
+ import { DEFAULT_ALLOWED_TYPES } from '../FilePicker/constants';
14
17
 
15
18
  import { DEFAULT_MAX_SIZE } from './constants';
16
19
  import {
17
- getDocumentPickerType,
20
+ getMimeType,
18
21
  getInitialValuesFrom,
19
22
  isFileFormatInvalid,
20
23
  isFileSizeInvaid,
@@ -38,16 +41,16 @@ const MultipleFilePicker = ({
38
41
  onChange,
39
42
  onError: onError_,
40
43
  onMaxSizeError: onMaxSizeError_,
44
+ permissionPrompt,
41
45
  pickerText,
42
46
  style,
43
47
  title,
44
48
  value
45
49
  }) => {
46
50
  const { files: values } = value || {};
47
-
48
51
  const [newFile, setNewFile] = useState(null);
49
52
  const [uploadedFiles, setUploadedFiles] = useState(isArray(values) ? getInitialValuesFrom(values) : []);
50
- const [rawFiles, setRawFiles] = useState(isArray(values) ? values : []);
53
+ const [rawFiles, setRawFiles] = useState(Array.isArray(values) ? values : []);
51
54
 
52
55
  const [isDrawerOpen, setIsDrawerOpen] = useState(false);
53
56
  const closeDrawer = () => setIsDrawerOpen(false);
@@ -64,44 +67,129 @@ const MultipleFilePicker = ({
64
67
 
65
68
  useEffect(() => {
66
69
  onChange?.(isEmpty(rawFiles) ? null : { files: rawFiles });
67
- }, [rawFiles]);
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ }, [rawFiles, onChange]);
68
72
 
69
73
  useEffect(() => {
70
74
  if (newFile) {
71
- setUploadedFiles([...uploadedFiles, newFile.uploadFile]);
72
- setRawFiles([...rawFiles, newFile.rawFile]);
75
+ setUploadedFiles(prev => [...prev, newFile.uploadFile]);
76
+ setRawFiles(prev => [...prev, newFile.rawFile]);
73
77
  }
74
78
  }, [newFile]);
75
79
 
80
+ const remainingSlots = () => Math.max((maxFiles || 0) - uploadedFiles.length, 0);
81
+
82
+ const handleAssets = async response => {
83
+ if (!response || response.didCancel) {
84
+ return;
85
+ }
86
+ if (response.errorCode) {
87
+ onError(response.errorCode);
88
+ return;
89
+ }
90
+ closeDrawer();
91
+ response.assets.forEach(async asset => {
92
+ const file = await retrieveFile(asset.uri, asset.type);
93
+ if (!file) {
94
+ onError(response.errorCode || 'No se pudo obtener el archivo');
95
+ return;
96
+ }
97
+ if (isFileTypeInvalid({ type: asset.type }, allowedTypes, fileTypeError, onError)) return;
98
+ if (isFileSizeInvaid({ size: file.size }, maxFileByteSize, onMaxSizeError)) return;
99
+ setNewFile({
100
+ uploadFile: { name: file.data.name, size: file.data.size },
101
+ rawFile: blobToFile(file, asset.type)
102
+ });
103
+ });
104
+ };
105
+
76
106
  const onPickFiles = async () => {
77
- const documents = await DocumentPicker.pick({
78
- allowMultiSelection: true,
79
- type: isEmpty(allowedTypes) ? DocumentPicker.types.allFiles : allowedTypes.map(getDocumentPickerType)
107
+ if (!ExpoDocumentPicker || typeof ExpoDocumentPicker.getDocumentAsync !== 'function') {
108
+ // eslint-disable-next-line no-console
109
+ console.error('No se encontró un ExpoDocumentPicker válido. instala "expo-document-picker".');
110
+ return;
111
+ }
112
+
113
+ const normalizeTypes = types => {
114
+ if (!types || types.length === 0) return DEFAULT_ALLOWED_TYPES;
115
+ const hasMime = types.some(t => typeof t === 'string' && t.includes('/'));
116
+ if (hasMime) return types;
117
+ const mapped = types.map(getMimeType).filter(Boolean);
118
+ return mapped.length ? mapped : DEFAULT_ALLOWED_TYPES;
119
+ };
120
+
121
+ const types = normalizeTypes(allowedTypes);
122
+ const result = await ExpoDocumentPicker.getDocumentAsync({
123
+ multiple: true,
124
+ type: types
80
125
  });
81
126
 
82
- if (uploadedFiles.length + documents.length > maxFiles)
127
+ if (result.canceled) {
128
+ return;
129
+ }
130
+
131
+ const documents = result.assets || [];
132
+
133
+ const slots = remainingSlots();
134
+ if (slots <= 0) {
83
135
  onError('La cantidad de archivos supera al máximo permitido');
84
- else {
85
- closeDrawer();
86
- documents.forEach(async document => {
87
- if (!uploadedFiles.some(({ name }) => name === document.name)) {
88
- if (isFileTypeInvalid(document, allowedTypes, fileTypeError, onError)) return;
89
- if (isFileSizeInvaid(document, maxFileByteSize, onMaxSizeError)) return;
90
- const file = await retrieveFile(document.uri, document.type, document.name);
91
- if (await isFileFormatInvalid(file, allowedPDFUploadSizes, onError)) return;
92
- setNewFile({
93
- uploadFile: { name: document.name, size: document.size },
94
- rawFile: blobToFile(file, document.type)
95
- });
96
- }
136
+ return;
137
+ }
138
+
139
+ closeDrawer();
140
+
141
+ let added = 0;
142
+ // eslint-disable-next-line no-restricted-syntax
143
+ for (const asset of documents) {
144
+ if (added >= slots) break;
145
+ // eslint-disable-next-line no-await-in-loop
146
+ const document = {
147
+ uri: asset.uri,
148
+ type: asset.mimeType || asset.type || 'application/octet-stream',
149
+ size: asset.size,
150
+ name: asset.name || 'document'
151
+ };
152
+
153
+ if (uploadedFiles.some(({ name }) => name === document.name)) {
154
+ // eslint-disable-next-line no-continue
155
+ continue;
156
+ }
157
+ if (isFileTypeInvalid(document, allowedTypes, fileTypeError, onError)) {
158
+ // eslint-disable-next-line no-continue
159
+ continue;
160
+ }
161
+ if (isFileSizeInvaid(document, maxFileByteSize, onMaxSizeError)) {
162
+ // eslint-disable-next-line no-continue
163
+ continue;
164
+ }
165
+
166
+ // eslint-disable-next-line no-await-in-loop
167
+ const file = await retrieveFile(document.uri, document.type, document.name);
168
+ // eslint-disable-next-line no-await-in-loop
169
+ if (await isFileFormatInvalid(file, allowedPDFUploadSizes, onError)) {
170
+ // eslint-disable-next-line no-continue
171
+ continue;
172
+ }
173
+
174
+ setNewFile({
175
+ uploadFile: { name: document.name, size: document.size },
176
+ rawFile: blobToFile(file, document.type)
97
177
  });
178
+ added += 1;
179
+ }
180
+
181
+ if (documents.length > slots) {
182
+ onError('Algunos archivos no se agregaron por superar el máximo permitido');
98
183
  }
99
184
  };
100
185
 
101
186
  // eslint-disable-next-line consistent-return
102
187
  const requestPermissionCamera = async () => {
103
188
  try {
104
- const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
189
+ const granted = await PermissionsAndroid.request(
190
+ PermissionsAndroid.PERMISSIONS.CAMERA,
191
+ permissionPrompt?.androidRationale
192
+ );
105
193
  return granted === PermissionsAndroid.RESULTS.GRANTED
106
194
  ? launchCamera
107
195
  : { errorCode: 'Son necesarios permisos para utilizar la cámara' };
@@ -111,14 +199,50 @@ const MultipleFilePicker = ({
111
199
  };
112
200
 
113
201
  const handleShowImagePicker = sourceImage => {
202
+ if (
203
+ !ExpoImagePicker ||
204
+ (!ExpoImagePicker.launchImageLibraryAsync && !ExpoImagePicker.launchCameraAsync)
205
+ ) {
206
+ // eslint-disable-next-line no-console
207
+ console.error('No se encontró un ExpoImagePicker válido. instala "expo-image-picker".');
208
+ return undefined;
209
+ }
210
+ if (remainingSlots() <= 0) {
211
+ onError('La cantidad de archivos supera al máximo permitido');
212
+ return undefined;
213
+ }
214
+ if (!sourceImage || typeof sourceImage !== 'function') {
215
+ if (ExpoImagePicker?.launchImageLibraryAsync) {
216
+ const expoGalleryAdapter = async (options, cb) => {
217
+ try {
218
+ const selectionLimit = options?.selectionLimit ?? 1;
219
+ const mediaTypes = ExpoImagePicker.MediaType ? [ExpoImagePicker.MediaType.image] : undefined;
220
+ const result = await ExpoImagePicker.launchImageLibraryAsync({
221
+ mediaTypes,
222
+ allowsMultipleSelection: selectionLimit && selectionLimit > 1,
223
+ quality: 1
224
+ });
225
+ cb?.({ assets: result?.assets || [], didCancel: result?.canceled });
226
+ } catch (e) {
227
+ cb?.({ didCancel: true, errorCode: String(e) });
228
+ }
229
+ };
230
+ handleShowImagePicker(expoGalleryAdapter);
231
+ return undefined;
232
+ }
233
+ onError('No hay selector de imágenes disponible en este entorno.');
234
+ return undefined;
235
+ }
236
+
114
237
  if (sourceImage?.errorCode) {
115
238
  onError(sourceImage.errorCode);
116
- return;
239
+ return undefined;
117
240
  }
241
+ closeDrawer();
118
242
  sourceImage(
119
243
  {
120
244
  mediaType: 'photo',
121
- selectionLimit: Math.max(maxFiles - uploadedFiles.length, 0)
245
+ selectionLimit: Math.max(remainingSlots(), 1)
122
246
  },
123
247
  async response => {
124
248
  if (response.didCancel) return;
@@ -134,8 +258,8 @@ const MultipleFilePicker = ({
134
258
  onError(response.errorCode);
135
259
  return;
136
260
  }
137
- if (isFileTypeInvalid(file, allowedTypes, fileTypeError, onError)) return;
138
- if (isFileSizeInvaid(file, maxFileByteSize, onMaxSizeError)) return;
261
+ if (isFileTypeInvalid({ type: asset.type }, allowedTypes, fileTypeError, onError)) return;
262
+ if (isFileSizeInvaid({ size: file.size }, maxFileByteSize, onMaxSizeError)) return;
139
263
 
140
264
  setNewFile({
141
265
  uploadFile: { name: file.data.name, size: file.data.size },
@@ -144,12 +268,128 @@ const MultipleFilePicker = ({
144
268
  });
145
269
  }
146
270
  );
271
+ return undefined;
147
272
  };
148
273
 
149
- const onPickCamera = async () =>
274
+ const onPickCamera = async () => {
275
+ if (
276
+ !ExpoImagePicker ||
277
+ (!ExpoImagePicker.launchImageLibraryAsync && !ExpoImagePicker.launchCameraAsync)
278
+ ) {
279
+ // eslint-disable-next-line no-console
280
+ console.error('No se encontró un ExpoImagePicker válido. instala "expo-image-picker".');
281
+ return undefined;
282
+ }
283
+ if (remainingSlots() <= 0) {
284
+ onError('La cantidad de archivos supera al máximo permitido');
285
+ return undefined;
286
+ }
287
+ if (ExpoImagePicker?.launchCameraAsync) {
288
+ const openCamera = async () => {
289
+ try {
290
+ const mediaTypes = ExpoImagePicker.MediaType ? [ExpoImagePicker.MediaType.image] : undefined;
291
+ closeDrawer();
292
+ const result = await ExpoImagePicker.launchCameraAsync({
293
+ mediaTypes,
294
+ allowsMultipleSelection: false,
295
+ quality: 1
296
+ });
297
+ await handleAssets({ assets: result?.assets || [], didCancel: result?.canceled }, 'expo:camera');
298
+ } catch (e) {
299
+ await handleAssets({ didCancel: true, errorCode: String(e) }, 'expo:camera');
300
+ }
301
+ };
302
+ const askAndOpenCamera = async () => {
303
+ try {
304
+ const perm = await ExpoImagePicker.requestCameraPermissionsAsync?.();
305
+ if (!perm || perm.status !== 'granted') {
306
+ onError('Son necesarios permisos para utilizar la cámara');
307
+ return;
308
+ }
309
+ openCamera();
310
+ } catch (e) {
311
+ onError('Son necesarios permisos para utilizar la cámara');
312
+ }
313
+ };
314
+
315
+ const pre = permissionPrompt?.expoPrePrompt;
316
+ if (pre?.title || pre?.message) {
317
+ Alert.alert(
318
+ pre.title || 'Permiso de cámara',
319
+ pre.message || 'Necesitamos acceso a la cámara para tomar fotos.',
320
+ [
321
+ { text: pre.cancelText || 'Cancelar', style: 'cancel' },
322
+ { text: pre.confirmText || 'Continuar', onPress: askAndOpenCamera }
323
+ ]
324
+ );
325
+ } else {
326
+ askAndOpenCamera();
327
+ }
328
+ return undefined;
329
+ }
150
330
  handleShowImagePicker(IS_IOS ? launchCamera : await requestPermissionCamera());
331
+ return undefined;
332
+ };
333
+
334
+ const onPickGallery = () => {
335
+ if (
336
+ !ExpoImagePicker ||
337
+ (!ExpoImagePicker.launchImageLibraryAsync && !ExpoImagePicker.launchCameraAsync)
338
+ ) {
339
+ // eslint-disable-next-line no-console
340
+ console.error('No se encontró un ExpoImagePicker válido. instala "expo-image-picker".');
341
+ return undefined;
342
+ }
343
+ if (remainingSlots() <= 0) {
344
+ onError('La cantidad de archivos supera al máximo permitido');
345
+ return undefined;
346
+ }
347
+ if (ExpoImagePicker?.launchImageLibraryAsync) {
348
+ const openGallery = async () => {
349
+ try {
350
+ const mediaTypes = ExpoImagePicker.MediaType ? [ExpoImagePicker.MediaType.image] : undefined;
351
+ closeDrawer();
352
+ const result = await ExpoImagePicker.launchImageLibraryAsync({
353
+ mediaTypes,
354
+ allowsMultipleSelection: false,
355
+ quality: 1
356
+ });
357
+ await handleAssets({ assets: result?.assets || [], didCancel: result?.canceled }, 'expo:gallery');
358
+ } catch (e) {
359
+ await handleAssets({ didCancel: true, errorCode: String(e) }, 'expo:gallery');
360
+ }
361
+ };
151
362
 
152
- const onPickGallery = () => handleShowImagePicker(launchImageLibrary);
363
+ const askPermissionAndOpen = async () => {
364
+ try {
365
+ const perm = await ExpoImagePicker.requestMediaLibraryPermissionsAsync?.();
366
+ if (!perm || perm.status !== 'granted') {
367
+ onError('Son necesarios permisos para acceder a tus fotos');
368
+ return;
369
+ }
370
+ openGallery();
371
+ } catch (e) {
372
+ onError('Son necesarios permisos para acceder a tus fotos');
373
+ }
374
+ };
375
+
376
+ const pre = permissionPrompt?.expoGalleryPrePrompt;
377
+ if (pre?.title || pre?.message) {
378
+ Alert.alert(
379
+ pre.title || 'Acceso a fotos',
380
+ pre.message || 'Necesitamos acceder a tu galería para seleccionar imágenes.',
381
+ [
382
+ { text: pre.cancelText || 'Cancelar', style: 'cancel' },
383
+ { text: pre.confirmText || 'Continuar', onPress: askPermissionAndOpen }
384
+ ]
385
+ );
386
+ } else {
387
+ askPermissionAndOpen();
388
+ }
389
+ return undefined;
390
+ }
391
+ return handleShowImagePicker(launchImageLibrary);
392
+ };
153
393
 
154
394
  const handleDeleteFile = index => {
155
395
  onChange?.(null);