@grupalia/rn-ui-kit 0.15.0 → 0.16.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 (126) hide show
  1. package/lib/commonjs/assets/illustrations/camera.svg +87 -0
  2. package/lib/commonjs/components/CameraImageInput.js +478 -0
  3. package/lib/commonjs/components/CameraImageInput.js.map +1 -0
  4. package/lib/commonjs/components/CameraWrapperModal.js +255 -0
  5. package/lib/commonjs/components/CameraWrapperModal.js.map +1 -0
  6. package/lib/commonjs/components/FormikCameraImageInput.js +37 -0
  7. package/lib/commonjs/components/FormikCameraImageInput.js.map +1 -0
  8. package/lib/commonjs/components/ImagePickerBottomSheet.js +100 -0
  9. package/lib/commonjs/components/ImagePickerBottomSheet.js.map +1 -0
  10. package/lib/commonjs/components/PhotoPickerModal.js +98 -0
  11. package/lib/commonjs/components/PhotoPickerModal.js.map +1 -0
  12. package/lib/commonjs/components/Toasts.js +188 -0
  13. package/lib/commonjs/components/Toasts.js.map +1 -0
  14. package/lib/commonjs/components/index.js +66 -0
  15. package/lib/commonjs/components/index.js.map +1 -1
  16. package/lib/commonjs/components/svgs/Camera.js +17 -0
  17. package/lib/commonjs/components/svgs/Camera.js.map +1 -0
  18. package/lib/commonjs/hooks/index.js +41 -0
  19. package/lib/commonjs/hooks/index.js.map +1 -0
  20. package/lib/commonjs/hooks/useInternetConnectionStatus.js +182 -0
  21. package/lib/commonjs/hooks/useInternetConnectionStatus.js.map +1 -0
  22. package/lib/commonjs/types/svg.d.js +2 -0
  23. package/lib/commonjs/types/svg.d.js.map +1 -0
  24. package/lib/commonjs/utils/fileDirectoryUtils.js +19 -0
  25. package/lib/commonjs/utils/fileDirectoryUtils.js.map +1 -0
  26. package/lib/commonjs/utils/index.js +22 -0
  27. package/lib/commonjs/utils/index.js.map +1 -1
  28. package/lib/commonjs/utils/timeConstants.js +15 -0
  29. package/lib/commonjs/utils/timeConstants.js.map +1 -0
  30. package/lib/module/assets/illustrations/camera.svg +87 -0
  31. package/lib/module/components/CameraImageInput.js +471 -0
  32. package/lib/module/components/CameraImageInput.js.map +1 -0
  33. package/lib/module/components/CameraWrapperModal.js +250 -0
  34. package/lib/module/components/CameraWrapperModal.js.map +1 -0
  35. package/lib/module/components/FormikCameraImageInput.js +32 -0
  36. package/lib/module/components/FormikCameraImageInput.js.map +1 -0
  37. package/lib/module/components/ImagePickerBottomSheet.js +95 -0
  38. package/lib/module/components/ImagePickerBottomSheet.js.map +1 -0
  39. package/lib/module/components/PhotoPickerModal.js +93 -0
  40. package/lib/module/components/PhotoPickerModal.js.map +1 -0
  41. package/lib/module/components/Toasts.js +182 -0
  42. package/lib/module/components/Toasts.js.map +1 -0
  43. package/lib/module/components/index.js +6 -0
  44. package/lib/module/components/index.js.map +1 -1
  45. package/lib/module/components/svgs/Camera.js +12 -0
  46. package/lib/module/components/svgs/Camera.js.map +1 -0
  47. package/lib/module/hooks/index.js +6 -0
  48. package/lib/module/hooks/index.js.map +1 -0
  49. package/lib/module/hooks/useInternetConnectionStatus.js +177 -0
  50. package/lib/module/hooks/useInternetConnectionStatus.js.map +1 -0
  51. package/lib/module/types/svg.d.js +2 -0
  52. package/lib/module/types/svg.d.js.map +1 -0
  53. package/lib/module/utils/fileDirectoryUtils.js +14 -0
  54. package/lib/module/utils/fileDirectoryUtils.js.map +1 -0
  55. package/lib/module/utils/index.js +2 -0
  56. package/lib/module/utils/index.js.map +1 -1
  57. package/lib/module/utils/timeConstants.js +12 -0
  58. package/lib/module/utils/timeConstants.js.map +1 -0
  59. package/lib/typescript/commonjs/components/CameraImageInput.d.ts +41 -0
  60. package/lib/typescript/commonjs/components/CameraImageInput.d.ts.map +1 -0
  61. package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts +18 -0
  62. package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts.map +1 -0
  63. package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts +14 -0
  64. package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts +15 -0
  66. package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts +19 -0
  68. package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/components/Toasts.d.ts +3 -0
  70. package/lib/typescript/commonjs/components/Toasts.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/components/index.d.ts +6 -0
  72. package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/components/svgs/Camera.d.ts +9 -0
  74. package/lib/typescript/commonjs/components/svgs/Camera.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
  76. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts +20 -0
  78. package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts +3 -0
  80. package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/utils/index.d.ts +2 -0
  82. package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/utils/timeConstants.d.ts +10 -0
  84. package/lib/typescript/commonjs/utils/timeConstants.d.ts.map +1 -0
  85. package/lib/typescript/module/components/CameraImageInput.d.ts +41 -0
  86. package/lib/typescript/module/components/CameraImageInput.d.ts.map +1 -0
  87. package/lib/typescript/module/components/CameraWrapperModal.d.ts +18 -0
  88. package/lib/typescript/module/components/CameraWrapperModal.d.ts.map +1 -0
  89. package/lib/typescript/module/components/FormikCameraImageInput.d.ts +14 -0
  90. package/lib/typescript/module/components/FormikCameraImageInput.d.ts.map +1 -0
  91. package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts +15 -0
  92. package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts.map +1 -0
  93. package/lib/typescript/module/components/PhotoPickerModal.d.ts +19 -0
  94. package/lib/typescript/module/components/PhotoPickerModal.d.ts.map +1 -0
  95. package/lib/typescript/module/components/Toasts.d.ts +3 -0
  96. package/lib/typescript/module/components/Toasts.d.ts.map +1 -0
  97. package/lib/typescript/module/components/index.d.ts +6 -0
  98. package/lib/typescript/module/components/index.d.ts.map +1 -1
  99. package/lib/typescript/module/components/svgs/Camera.d.ts +9 -0
  100. package/lib/typescript/module/components/svgs/Camera.d.ts.map +1 -0
  101. package/lib/typescript/module/hooks/index.d.ts +4 -0
  102. package/lib/typescript/module/hooks/index.d.ts.map +1 -0
  103. package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts +20 -0
  104. package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts.map +1 -0
  105. package/lib/typescript/module/utils/fileDirectoryUtils.d.ts +3 -0
  106. package/lib/typescript/module/utils/fileDirectoryUtils.d.ts.map +1 -0
  107. package/lib/typescript/module/utils/index.d.ts +2 -0
  108. package/lib/typescript/module/utils/index.d.ts.map +1 -1
  109. package/lib/typescript/module/utils/timeConstants.d.ts +10 -0
  110. package/lib/typescript/module/utils/timeConstants.d.ts.map +1 -0
  111. package/package.json +27 -2
  112. package/src/components/CameraImageInput.tsx +610 -0
  113. package/src/components/CameraWrapperModal.tsx +309 -0
  114. package/src/components/FormikCameraImageInput.tsx +39 -0
  115. package/src/components/ImagePickerBottomSheet.tsx +109 -0
  116. package/src/components/PhotoPickerModal.tsx +116 -0
  117. package/src/components/Toasts.tsx +200 -0
  118. package/src/components/index.ts +6 -0
  119. package/src/components/svgs/Camera.tsx +14 -0
  120. package/src/hooks/index.ts +3 -0
  121. package/src/hooks/useBreakpoints.ts +20 -0
  122. package/src/hooks/useInternetConnectionStatus.ts +221 -0
  123. package/src/hooks/useIsAboveBreakpoint.ts +8 -0
  124. package/src/utils/fileDirectoryUtils.ts +12 -0
  125. package/src/utils/index.ts +2 -0
  126. package/src/utils/timeConstants.ts +19 -0
@@ -0,0 +1,309 @@
1
+ import { useAppState } from '@react-native-community/hooks';
2
+ import { useIsFocused } from '@react-navigation/native';
3
+ import { captureException } from '@sentry/react-native';
4
+ import { useMutation } from '@tanstack/react-query';
5
+ import clsx from 'clsx';
6
+ import { styled } from 'nativewind';
7
+ import {
8
+ ComponentProps, useRef, useState, useCallback,
9
+ } from 'react';
10
+ import toast from 'react-hot-toast/headless';
11
+ import { Linking } from 'react-native';
12
+ import {
13
+ ArrowLongLeftIcon, ArrowPathIcon, ArrowUturnLeftIcon, CheckIcon,
14
+ } from 'react-native-heroicons/outline';
15
+ import {
16
+ PhotoFile,
17
+ Camera as RNVisionCamera,
18
+ useCameraPermission,
19
+ useCameraDevice,
20
+ useCameraFormat,
21
+ FormatFilter,
22
+ CameraPosition,
23
+ } from 'react-native-vision-camera';
24
+
25
+ import {
26
+ View, Modal, Pressable, Image,
27
+ } from '../hoc-components';
28
+ import BaseButton from './BaseButton';
29
+ import BaseIcon from './BaseIcon';
30
+ import BaseText from './BaseText';
31
+ import PressableOpacity from './PressableOpacity';
32
+ import { useIsAboveBreakpoint } from '../hooks/useIsAboveBreakpoint';
33
+ import { ensureDirExists } from '../utils/fileDirectoryUtils';
34
+ import CameraSvg from './svgs/Camera';
35
+ import Toasts from './Toasts';
36
+
37
+ interface PermissionCardProps {
38
+ onPermissionRequestPress: () => void;
39
+ }
40
+
41
+ export type CameraWrapperModalImage = PhotoFile & { uri: string };
42
+
43
+ function PermissionCard({ onPermissionRequestPress }: PermissionCardProps) {
44
+ const largePhone = useIsAboveBreakpoint('xs');
45
+
46
+ return (
47
+ <View className={clsx(
48
+ 'w-full flex-col items-center rounded-lg bg-white p-4',
49
+ largePhone ? 'mt-10 space-y-10' : 'space-y-4',
50
+ )}
51
+ >
52
+ <BaseText className={clsx('text-center font-semibold', largePhone && 'text-xl')}>
53
+ Necesitamos permiso para acceder a la camara
54
+ </BaseText>
55
+ <CameraSvg
56
+ width="100%"
57
+ height={largePhone ? 200 : 100}
58
+ />
59
+ <BaseButton
60
+ className="w-full"
61
+ size={largePhone ? 'md' : 'xs'}
62
+ text="Permitir"
63
+ onPress={onPermissionRequestPress}
64
+ />
65
+ </View>
66
+ );
67
+ }
68
+
69
+ interface CameraPreviewProps {
70
+ onCapture: (photoFile: CameraWrapperModalImage) => void;
71
+ previewOverlay: JSX.Element | undefined;
72
+ filters: FormatFilter[];
73
+ allowDeviceSwitching: boolean;
74
+ isActive: boolean;
75
+ initialDevice: CameraPosition;
76
+ pathToSave?: string;
77
+ }
78
+
79
+ function CameraPreview({
80
+ onCapture,
81
+ previewOverlay,
82
+ filters,
83
+ isActive,
84
+ allowDeviceSwitching,
85
+ initialDevice,
86
+ pathToSave,
87
+ }: CameraPreviewProps) {
88
+ const cameraRef = useRef<RNVisionCamera>(null);
89
+ const [selectedDevice, setSelectedDevice] = useState<CameraPosition>(initialDevice);
90
+ const device = useCameraDevice(selectedDevice);
91
+ const format = useCameraFormat(device, filters);
92
+
93
+ const { mutate } = useMutation<PhotoFile, Error, void>({
94
+ mutationFn: async () => {
95
+ if (cameraRef.current) {
96
+ if (pathToSave) await ensureDirExists(pathToSave);
97
+
98
+ const file = await cameraRef.current.takePhoto({ path: pathToSave?.replace('file://', '') ?? undefined });
99
+
100
+ return file;
101
+ }
102
+
103
+ return Promise.reject(new Error('Camera ref is not defined'));
104
+ },
105
+ networkMode: 'always',
106
+ onSuccess: (photoFile) => onCapture({ ...photoFile, uri: `file://${photoFile.path}` }),
107
+ onError: (error) => {
108
+ captureException(error);
109
+ toast.error('Ocurrió un error al tomar la foto');
110
+ },
111
+ });
112
+
113
+ return (
114
+ <View className="flex h-full w-full flex-col">
115
+ {device ? (
116
+ <View className="relative h-full w-full flex-1 overflow-hidden rounded-xl border-2">
117
+ <RNVisionCamera
118
+ style={{ flex: 1, width: '100%', height: '100%' }}
119
+ ref={cameraRef}
120
+ device={device}
121
+ format={format}
122
+ isActive={isActive}
123
+ photo
124
+ />
125
+ {previewOverlay}
126
+ {allowDeviceSwitching && (
127
+ <PressableOpacity
128
+ onPress={() => setSelectedDevice(selectedDevice === 'back' ? 'front' : 'back')}
129
+ className="absolute bottom-8 right-8 flex h-12 w-12 items-center justify-center rounded-full bg-secondary"
130
+ >
131
+ <BaseIcon
132
+ icon={ArrowPathIcon}
133
+ className="h-full w-full"
134
+ color="utility-gray-200"
135
+ />
136
+ </PressableOpacity>
137
+ )}
138
+ </View>
139
+ ) : (
140
+ <View className="flex h-full w-full items-center justify-center bg-utility-gray-200">
141
+ <BaseText className="text-secondary">
142
+ No fue posible acceder a la camara
143
+ </BaseText>
144
+ </View>
145
+ )}
146
+ <PressableOpacity
147
+ onPress={() => mutate()}
148
+ className="my-10 h-20 w-20 self-center rounded-full border-4 border-white bg-black p-1"
149
+ >
150
+ <View
151
+ className="h-full w-full rounded-full bg-white"
152
+ />
153
+ </PressableOpacity>
154
+ </View>
155
+ );
156
+ }
157
+
158
+ interface PhotoFilePreviewProps {
159
+ photoFile: CameraWrapperModalImage;
160
+ onConfirm: () => void;
161
+ onCancel: () => void;
162
+ }
163
+
164
+ function PhotoFilePreview({ photoFile, onConfirm, onCancel }: PhotoFilePreviewProps) {
165
+ return (
166
+ <View className="flex h-full w-full flex-col items-center pb-10">
167
+ <View className="w-full flex-1 overflow-hidden rounded-xl">
168
+ <Image
169
+ style={{ width: 'auto', height: '100%' }}
170
+ source={{ uri: photoFile.uri }}
171
+ resizeMode="cover"
172
+ />
173
+ </View>
174
+ <View className="mt-12 flex flex-row items-center space-x-16">
175
+ <Pressable
176
+ onPress={onCancel}
177
+ >
178
+ <BaseIcon
179
+ icon={ArrowUturnLeftIcon}
180
+ size={64}
181
+ color="fg-error-primary"
182
+ />
183
+ </Pressable>
184
+ <Pressable
185
+ onPress={onConfirm}
186
+ className="self-end"
187
+ >
188
+ <BaseIcon
189
+ icon={CheckIcon}
190
+ size={64}
191
+ color="fg-success-primary"
192
+ />
193
+ </Pressable>
194
+ </View>
195
+ </View>
196
+ );
197
+ }
198
+
199
+ interface Props extends ComponentProps<typeof Modal> {
200
+ onCapture: (photoFile: CameraWrapperModalImage) => void;
201
+ onClose: () => void;
202
+ filters?: FormatFilter[];
203
+ previewOverlay?: JSX.Element;
204
+ allowDeviceSwitching?: boolean;
205
+ initialDevice?: CameraPosition;
206
+ pathToSave?: string;
207
+ }
208
+
209
+ function CameraWrapperModal({
210
+ onCapture,
211
+ onClose,
212
+ filters = [{ photoResolution: { width: 720, height: 1280 } }],
213
+ previewOverlay = undefined,
214
+ allowDeviceSwitching = true,
215
+ initialDevice = 'back',
216
+ pathToSave = undefined,
217
+ ...modalProps
218
+ }: Props) {
219
+ const [photoFile, setPhotoFile] = useState<CameraWrapperModalImage | null>(null);
220
+ const [permissionRequested, setPermissionRequested] = useState(false);
221
+
222
+ const isFocused = useIsFocused();
223
+ const appState = useAppState();
224
+ const isActive = isFocused && appState === 'active';
225
+
226
+ const { hasPermission, requestPermission } = useCameraPermission();
227
+
228
+ const handlePermissionRequestPress = useCallback(() => {
229
+ if (!permissionRequested) {
230
+ setPermissionRequested(true);
231
+ requestPermission();
232
+ } else {
233
+ Linking.openSettings();
234
+ }
235
+ }, [permissionRequested, requestPermission]);
236
+
237
+ const handleCaptureConfirm = useCallback(() => {
238
+ if (photoFile) {
239
+ onCapture({
240
+ ...photoFile,
241
+ uri: `file://${photoFile.path}`,
242
+ });
243
+ setPhotoFile(null);
244
+ }
245
+ }, [onCapture, photoFile]);
246
+
247
+ const removePhotoFile = useCallback(() => {
248
+ setPhotoFile(null);
249
+ }, [setPhotoFile]);
250
+
251
+ let content;
252
+
253
+ if (!hasPermission) {
254
+ content = (
255
+ <PermissionCard
256
+ onPermissionRequestPress={handlePermissionRequestPress}
257
+ />
258
+ );
259
+ } else if (photoFile) {
260
+ content = (
261
+ <PhotoFilePreview
262
+ photoFile={photoFile}
263
+ onCancel={removePhotoFile}
264
+ onConfirm={handleCaptureConfirm}
265
+ />
266
+ );
267
+ } else {
268
+ content = (
269
+ <CameraPreview
270
+ isActive={isActive}
271
+ filters={filters}
272
+ onCapture={setPhotoFile}
273
+ previewOverlay={previewOverlay}
274
+ allowDeviceSwitching={allowDeviceSwitching}
275
+ initialDevice={initialDevice}
276
+ pathToSave={pathToSave}
277
+ />
278
+ );
279
+ }
280
+
281
+ return (
282
+ <Modal
283
+ animationType="slide"
284
+ statusBarTranslucent
285
+ {...modalProps}
286
+ >
287
+ <Toasts />
288
+ <View className="flex h-full w-full flex-col items-center bg-black px-4 pt-12">
289
+ <View className="self-start py-4">
290
+ <Pressable
291
+ hitSlop={10}
292
+ onPress={onClose}
293
+ >
294
+ <BaseIcon
295
+ icon={ArrowLongLeftIcon}
296
+ color="fg-brand-primary"
297
+ size={32}
298
+ />
299
+ </Pressable>
300
+ </View>
301
+ <View className="mb-4 w-full flex-1">
302
+ {content}
303
+ </View>
304
+ </View>
305
+ </Modal>
306
+ );
307
+ }
308
+
309
+ export default styled(CameraWrapperModal);
@@ -0,0 +1,39 @@
1
+ import { ErrorMessage, useField } from 'formik';
2
+ import { styled } from 'nativewind';
3
+ import React from 'react';
4
+ import { ViewProps } from 'react-native';
5
+
6
+ import BaseText from './BaseText';
7
+ import CameraImageInput, { CameraImageInputProps } from './CameraImageInput';
8
+
9
+ interface FormikCameraImageInputProps extends Omit<CameraImageInputProps, 'value' | 'onValueChange' | 'hasError'>, ViewProps {
10
+ name: string;
11
+ }
12
+
13
+ function FormikCameraImageInput({
14
+ name,
15
+ ...props
16
+ }: FormikCameraImageInputProps) {
17
+ const [field, meta, helpers] = useField(name);
18
+
19
+ const hasError = !!(meta.error && meta.touched);
20
+
21
+ return (
22
+ <>
23
+ <CameraImageInput
24
+ value={field.value}
25
+ onValueChange={helpers.setValue}
26
+ hasError={hasError}
27
+ showImages
28
+ {...props}
29
+ />
30
+ {hasError && (
31
+ <ErrorMessage name={name}>
32
+ {(msg) => <BaseText className="mt-1 text-sm text-error-primary">{msg}</BaseText>}
33
+ </ErrorMessage>
34
+ )}
35
+ </>
36
+ );
37
+ }
38
+
39
+ export default styled(FormikCameraImageInput);
@@ -0,0 +1,109 @@
1
+ import { BottomSheetModal } from '@gorhom/bottom-sheet';
2
+ import {
3
+ ComponentProps,
4
+ useState, useRef, useEffect, useCallback,
5
+ } from 'react';
6
+
7
+ import BaseBottomSheetModal from './BaseBottomSheetModal';
8
+ import BaseText from './BaseText';
9
+ import CameraWrapperModal, { CameraWrapperModalImage } from './CameraWrapperModal';
10
+ import PhotoPickerModal, { PhotoPickerModalImage } from './PhotoPickerModal';
11
+ import PressableOpacity from './PressableOpacity';
12
+ import { View } from '../hoc-components';
13
+
14
+ export type ImagePickerBottomSheetImage = PhotoPickerModalImage | CameraWrapperModalImage;
15
+
16
+ interface Props extends Partial<ComponentProps<typeof BaseBottomSheetModal>> {
17
+ open: boolean;
18
+ onImagePick: (image: ImagePickerBottomSheetImage) => void;
19
+ onDismiss: () => void;
20
+ galleryAllowed?: boolean;
21
+ cameraWrapperProps?: Omit<ComponentProps<typeof CameraWrapperModal>, 'onCapture' | 'onClose'>;
22
+ }
23
+
24
+ export default function ImagePickerBottomSheet({
25
+ open,
26
+ onImagePick,
27
+ onDismiss,
28
+ galleryAllowed = true,
29
+ cameraWrapperProps = {},
30
+ ...props
31
+ }: Props) {
32
+ const bottomSheetModalRef = useRef<BottomSheetModal>(null);
33
+ const [isCameraModalOpen, setIsCameraModalOpen] = useState(false);
34
+ const [isPhotoPickerModalOpen, setIsPhotoPickerModalOpen] = useState(false);
35
+
36
+ useEffect(() => {
37
+ if (open) {
38
+ if (galleryAllowed) {
39
+ bottomSheetModalRef.current?.present();
40
+ } else {
41
+ setIsCameraModalOpen(true);
42
+ }
43
+ } else {
44
+ bottomSheetModalRef.current?.dismiss();
45
+ setIsCameraModalOpen(false);
46
+ setIsPhotoPickerModalOpen(false);
47
+ }
48
+ }, [open, galleryAllowed]);
49
+
50
+ const handleClose = useCallback(() => {
51
+ setIsCameraModalOpen(false);
52
+ setIsPhotoPickerModalOpen(false);
53
+ onDismiss();
54
+ }, [onDismiss]);
55
+
56
+ const onPhotoPick = useCallback((image: PhotoPickerModalImage) => {
57
+ onImagePick(image);
58
+ }, [onImagePick]);
59
+
60
+ const onCameraImagePick = useCallback((image: CameraWrapperModalImage) => {
61
+ onImagePick(({
62
+ uri: `file://${image.path}`,
63
+ width: image.width,
64
+ height: image.height,
65
+ }));
66
+ }, [onImagePick]);
67
+
68
+ return (
69
+ <>
70
+ <BaseBottomSheetModal
71
+ enableDynamicSizing
72
+ ref={bottomSheetModalRef}
73
+ onDismiss={handleClose}
74
+ {...props}
75
+ >
76
+ <View className="flex flex-col items-center space-y-8 py-8">
77
+ <PressableOpacity
78
+ className="flex w-1/2 flex-row items-center justify-between space-x-4 p-2"
79
+ onPress={() => setIsCameraModalOpen(true)}
80
+ >
81
+ <BaseText className="text-xl">Tomar foto</BaseText>
82
+ <BaseText className="text-xl">📸</BaseText>
83
+ </PressableOpacity>
84
+ <PressableOpacity
85
+ className="flex w-1/2 flex-row items-center justify-between space-x-4 p-2"
86
+ onPress={() => setIsPhotoPickerModalOpen(true)}
87
+ >
88
+ <BaseText className="text-xl">Abrir galería</BaseText>
89
+ <BaseText className="text-xl">🌅</BaseText>
90
+ </PressableOpacity>
91
+ </View>
92
+ </BaseBottomSheetModal>
93
+ <PhotoPickerModal
94
+ visible={isPhotoPickerModalOpen}
95
+ onImagePick={onPhotoPick}
96
+ onClose={handleClose}
97
+ onRequestClose={handleClose}
98
+ />
99
+ <CameraWrapperModal
100
+ visible={isCameraModalOpen}
101
+ className="h-full w-full"
102
+ onCapture={onCameraImagePick}
103
+ onClose={handleClose}
104
+ onRequestClose={handleClose}
105
+ {...cameraWrapperProps}
106
+ />
107
+ </>
108
+ );
109
+ }
@@ -0,0 +1,116 @@
1
+ import { styled } from 'nativewind';
2
+ import { ComponentProps, useEffect, useState } from 'react';
3
+ import toast from 'react-hot-toast/headless';
4
+ import {
5
+ launchImageLibrary, PhotoQuality, Asset,
6
+ } from 'react-native-image-picker';
7
+
8
+ import BaseButton from './BaseButton';
9
+ import BaseText from './BaseText';
10
+ import { View, Modal, Image } from '../hoc-components';
11
+ import PressableOpacity from './PressableOpacity';
12
+
13
+ export type PhotoPickerModalImage = Asset & { uri: string };
14
+
15
+ interface Props extends ComponentProps<typeof Modal> {
16
+ onImagePick: (image: PhotoPickerModalImage) => void;
17
+ onClose: () => void;
18
+ imageOptions?: {
19
+ maxWidth?: number;
20
+ maxHeight?: number;
21
+ quality?: PhotoQuality;
22
+ includeBase64?: boolean;
23
+ };
24
+ }
25
+
26
+ function MediaLibraryModal({
27
+ onImagePick,
28
+ onClose,
29
+ visible,
30
+ imageOptions = {
31
+ maxWidth: 1024,
32
+ maxHeight: 1024,
33
+ quality: 0.8,
34
+ includeBase64: false,
35
+ },
36
+ ...props
37
+ }: Props) {
38
+ const [image, setImage] = useState<PhotoPickerModalImage | undefined>();
39
+
40
+ useEffect(() => {
41
+ const handleImageSelect = async () => {
42
+ try {
43
+ const result = await launchImageLibrary({
44
+ mediaType: 'photo',
45
+ selectionLimit: 1,
46
+ ...imageOptions,
47
+ });
48
+
49
+ if (result.didCancel) {
50
+ onClose();
51
+ return;
52
+ }
53
+
54
+ if (!result.assets?.length) {
55
+ return;
56
+ }
57
+
58
+ const asset = result.assets[0];
59
+ if (!asset.uri) {
60
+ toast.error('Error al seleccionar la imagen');
61
+ return;
62
+ }
63
+
64
+ setImage({
65
+ uri: asset.uri,
66
+ type: asset.type,
67
+ width: asset.width,
68
+ height: asset.height,
69
+ });
70
+ } catch {
71
+ toast.error('Error al seleccionar la imagen');
72
+ }
73
+ };
74
+
75
+ if (visible) {
76
+ handleImageSelect();
77
+ }
78
+ }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
79
+
80
+ return (
81
+ <Modal
82
+ visible={visible}
83
+ {...props}
84
+ >
85
+ <View className="flex h-full flex-col px-4 py-8">
86
+ <PressableOpacity
87
+ onPress={onClose}
88
+ hitSlop={6}
89
+ className="mb-4 self-end"
90
+ >
91
+ <BaseText className="font-medium text-gray-700">
92
+ Cancelar
93
+ </BaseText>
94
+ </PressableOpacity>
95
+ {image && (
96
+ <>
97
+ <View className="w-full flex-1 overflow-hidden rounded-xl">
98
+ <Image
99
+ className="h-full w-auto"
100
+ source={{ uri: image.uri }}
101
+ resizeMode="contain"
102
+ />
103
+ </View>
104
+ <BaseButton
105
+ text="Confirmar"
106
+ onPress={() => onImagePick(image)}
107
+ className="mt-4 w-full self-center"
108
+ />
109
+ </>
110
+ )}
111
+ </View>
112
+ </Modal>
113
+ );
114
+ }
115
+
116
+ export default styled(MediaLibraryModal);