@widergy/mobile-ui 2.0.2 → 3.4.1

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.
@@ -0,0 +1,447 @@
1
+ # FileDownloadManager
2
+
3
+ Módulo unificado y abstracto para manejar la descarga, almacenamiento, notificación y apertura de archivos en React Native con Expo.
4
+
5
+ ## 🎯 Características
6
+
7
+ - ✅ **Generación de archivos**: PDF, Excel (XLSX), JSON
8
+ - ✅ **Almacenamiento seguro**: Storage Access Framework (Android) y documentDirectory (iOS)
9
+ - ✅ **Notificaciones**: Notificación cuando el archivo está listo con tap-to-open
10
+ - ✅ **Visualización nativa**: Soporte para PDF viewer nativo en la app
11
+ - ✅ **Compartir**: Integración con Expo Sharing para archivos sin visor nativo
12
+ - ✅ **Colisión de nombres**: Agrega timestamp automáticamente si el archivo ya existe
13
+ - ✅ **Traducciones personalizables**: Sistema flexible de i18n por parámetros
14
+ - ✅ **Multiplataforma**: iOS y Android
15
+
16
+ ## 📦 Estructura
17
+
18
+ ```
19
+ src/services/FileDownloadManager/
20
+ ├── index.js # Módulo core (listo para @widergy/mobile-ui)
21
+ ├── adapter.js # Adapter específico del proyecto
22
+ ├── types.js # Tipos y constantes
23
+ └── README.md # Esta documentación
24
+ ```
25
+
26
+ ## 🚀 Uso Básico
27
+
28
+ ### Excel (XLSX)
29
+
30
+ ```javascript
31
+ import { downloadExcel } from 'services/FileDownloadManager/adapter';
32
+ import * as snackbarActions from 'actions/snackbar';
33
+
34
+ // En tu action o componente
35
+ const exportData = async (data, dispatch) => {
36
+ await downloadExcel(
37
+ data, // Array de objetos
38
+ 'Reporte_Facturas', // Nombre base del archivo
39
+ dispatch, // Redux dispatch (opcional)
40
+ snackbarActions, // Snackbar actions del proyecto
41
+ { // Traducciones personalizadas (opcional)
42
+ successDownload: 'Excel descargado exitosamente',
43
+ errorDownload: 'Error al descargar Excel'
44
+ },
45
+ { // Opciones adicionales
46
+ successCallback: ({ fileUri, fileName }) => {
47
+ console.log(`Archivo guardado: ${fileName}`);
48
+ },
49
+ failureCallback: (error) => {
50
+ console.error('Error:', error);
51
+ },
52
+ preventAutoOpen: false, // Auto-abrir después de guardar
53
+ hideNotification: false // Mostrar notificación
54
+ }
55
+ );
56
+ };
57
+ ```
58
+
59
+ ### PDF
60
+
61
+ ```javascript
62
+ import { downloadPDF } from 'services/FileDownloadManager/adapter';
63
+ import * as snackbarActions from 'actions/snackbar';
64
+
65
+ const savePDF = async (base64Content, dispatch) => {
66
+ await downloadPDF(
67
+ base64Content, // PDF en base64
68
+ 'Factura_12345', // Nombre base
69
+ dispatch, // Redux dispatch
70
+ snackbarActions, // Snackbar actions
71
+ { // Traducciones (opcional)
72
+ downloadReadyTitle: 'PDF listo',
73
+ downloadReadyBody: '{fileName} está listo para ver'
74
+ },
75
+ { // Opciones
76
+ preventAutoOpen: false // Se abrirá con visor nativo
77
+ }
78
+ );
79
+ };
80
+ ```
81
+
82
+ ### JSON
83
+
84
+ ```javascript
85
+ import { downloadJSON } from 'services/FileDownloadManager/adapter';
86
+ import * as snackbarActions from 'actions/snackbar';
87
+
88
+ const exportConfig = async (configData, dispatch) => {
89
+ await downloadJSON(
90
+ configData, // Objeto o array
91
+ 'configuracion', // Nombre base
92
+ dispatch, // Redux dispatch
93
+ snackbarActions, // Snackbar actions
94
+ {} // Traducciones (usa defaults si está vacío)
95
+ );
96
+ };
97
+ ```
98
+
99
+ ## 🔧 API del Módulo Core
100
+
101
+ ### `downloadFile(config)`
102
+
103
+ Función principal que maneja todo el flujo de descarga.
104
+
105
+ **Parámetros:**
106
+ ```typescript
107
+ {
108
+ type: 'pdf' | 'xlsx' | 'json', // Tipo de archivo
109
+ fileName: string, // Nombre base (sin extensión)
110
+ data: any, // Datos del archivo
111
+ dispatch?: Function, // Redux dispatch (opcional)
112
+ translations?: { // Traducciones personalizadas (opcional)
113
+ downloadReadyTitle?: string, // Título notificación
114
+ downloadReadyBody?: string, // Cuerpo notificación (usa {fileName})
115
+ shareFileTitle?: string, // Título diálogo compartir (usa {fileName})
116
+ shareError?: string, // Error al compartir
117
+ successDownload?: string, // Mensaje éxito
118
+ errorDownload?: string // Mensaje error
119
+ },
120
+ options?: {
121
+ hideNotification?: boolean, // Omitir notificación
122
+ preventAutoOpen?: boolean, // No abrir automáticamente
123
+ successCallback?: Function, // Callback de éxito
124
+ failureCallback?: Function, // Callback de error
125
+ showSnackbar?: Function // Función para mostrar snackbar
126
+ }
127
+ }
128
+ ```
129
+
130
+ **Retorna:**
131
+ ```typescript
132
+ Promise<{
133
+ success: boolean,
134
+ fileUri: string,
135
+ fileName: string
136
+ }>
137
+ ```
138
+
139
+ ### `openFile(fileUri, fileName, fileType, translations?)`
140
+
141
+ Abre un archivo previamente guardado.
142
+
143
+ ```javascript
144
+ import { openFile, FILE_TYPES } from 'services/FileDownloadManager/adapter';
145
+
146
+ await openFile(
147
+ 'file:///path/to/file.pdf',
148
+ 'documento.pdf',
149
+ FILE_TYPES.PDF,
150
+ { // Traducciones opcionales
151
+ shareFileTitle: 'Compartir {fileName}',
152
+ shareError: 'No se pudo compartir'
153
+ }
154
+ );
155
+ ```
156
+
157
+ ## 🌐 Sistema de Traducciones
158
+
159
+ El módulo utiliza un sistema flexible de traducciones que permite personalizar todos los mensajes sin depender de librerías externas como i18next.
160
+
161
+ ### Traducciones Disponibles
162
+
163
+ ```typescript
164
+ {
165
+ // Título de notificación cuando el archivo está listo
166
+ // Usado en: Notificación del sistema
167
+ downloadReadyTitle: string;
168
+
169
+ // Cuerpo de notificación con nombre del archivo
170
+ // Parámetros: {fileName} - se reemplaza automáticamente
171
+ // Usado en: Notificación del sistema
172
+ downloadReadyBody: string;
173
+
174
+ // Título del diálogo de compartir
175
+ // Parámetros: {fileName} - se reemplaza automáticamente
176
+ // Usado en: Diálogo nativo de compartir (iOS/Android)
177
+ shareFileTitle: string;
178
+
179
+ // Mensaje de error cuando falla compartir
180
+ // Usado en: Callback de error, snackbar
181
+ shareError: string;
182
+
183
+ // Mensaje de éxito para snackbar/toast
184
+ // Usado en: Snackbar después de descarga exitosa
185
+ successDownload: string;
186
+
187
+ // Mensaje de error para snackbar/toast
188
+ // Usado en: Snackbar cuando falla la descarga
189
+ errorDownload: string;
190
+ }
191
+ ```
192
+
193
+ ### Valores por Defecto (Español)
194
+
195
+ ```javascript
196
+ {
197
+ downloadReadyTitle: 'Archivo listo',
198
+ downloadReadyBody: '{fileName} está disponible',
199
+ shareFileTitle: 'Compartir {fileName}',
200
+ shareError: 'No se pudo compartir el archivo',
201
+ successDownload: 'Archivo descargado correctamente',
202
+ errorDownload: 'Error al descargar el archivo'
203
+ }
204
+ ```
205
+
206
+ ### Ejemplo: Traducciones en Inglés
207
+
208
+ ```javascript
209
+ import { downloadPDF } from 'services/FileDownloadManager/adapter';
210
+
211
+ const translations = {
212
+ downloadReadyTitle: 'File ready',
213
+ downloadReadyBody: '{fileName} is available',
214
+ shareFileTitle: 'Share {fileName}',
215
+ shareError: 'Could not share file',
216
+ successDownload: 'File downloaded successfully',
217
+ errorDownload: 'Error downloading file'
218
+ };
219
+
220
+ await downloadPDF(
221
+ pdfBase64,
222
+ 'Invoice_2024',
223
+ dispatch,
224
+ snackbarActions,
225
+ translations // Pasa las traducciones personalizadas
226
+ );
227
+ ```
228
+
229
+ ### Integración con i18next
230
+
231
+ Si tu proyecto usa i18next, puedes crear un helper:
232
+
233
+ ```javascript
234
+ // utils/fileManagerTranslations.js
235
+ import i18next from 'i18next';
236
+
237
+ export const getFileManagerTranslations = () => ({
238
+ downloadReadyTitle: i18next.t('fileManager.downloadReadyTitle'),
239
+ downloadReadyBody: i18next.t('fileManager.downloadReadyBody'),
240
+ shareFileTitle: i18next.t('fileManager.shareFileTitle'),
241
+ shareError: i18next.t('fileManager.shareError'),
242
+ successDownload: i18next.t('fileManager.successDownload'),
243
+ errorDownload: i18next.t('fileManager.errorDownload')
244
+ });
245
+
246
+ // Uso
247
+ import { getFileManagerTranslations } from 'utils/fileManagerTranslations';
248
+
249
+ await downloadExcel(
250
+ data,
251
+ fileName,
252
+ dispatch,
253
+ snackbarActions,
254
+ getFileManagerTranslations()
255
+ );
256
+ ```
257
+
258
+ ### Traducciones Parciales
259
+
260
+ No es necesario proveer todas las traducciones. Las que no se envíen usarán los valores por defecto:
261
+
262
+ ```javascript
263
+ // Solo personalizar algunos mensajes
264
+ await downloadJSON(
265
+ data,
266
+ 'config',
267
+ dispatch,
268
+ snackbarActions,
269
+ {
270
+ successDownload: 'JSON exportado!',
271
+ errorDownload: 'Falló la exportación'
272
+ // Resto usa valores por defecto
273
+ }
274
+ );
275
+ ```
276
+
277
+ ## 📱 Comportamiento por Tipo de Archivo
278
+
279
+ ### PDF
280
+ - ✅ **Visor nativo**: Se abre dentro de la app
281
+ - 📁 **Ubicación**: `documentDirectory` (iOS/Android)
282
+ - 🔔 **Notificación**: Sí, con tap-to-open
283
+ - 🔄 **Fallback**: Si falla visor nativo → Expo Sharing
284
+
285
+ ### XLSX (Excel)
286
+ - ❌ **Visor nativo**: No soportado
287
+ - 📁 **Ubicación**: `documentDirectory` (iOS/Android)
288
+ - 🔔 **Notificación**: Sí, con tap-to-share
289
+ - 🔄 **Comportamiento**: Siempre usa Expo Sharing para abrir
290
+
291
+ ### JSON
292
+ - ❌ **Visor nativo**: No soportado
293
+ - 📁 **Ubicación**: `documentDirectory` (iOS/Android)
294
+ - 🔔 **Notificación**: Sí, con tap-to-share
295
+ - 🔄 **Comportamiento**: Siempre usa Expo Sharing para abrir
296
+
297
+ ## 🔄 Migración desde Código Legacy
298
+
299
+ ### Antes (xlsDownload.js)
300
+ ```javascript
301
+ import { generateExcel } from 'utils/xlsDownload';
302
+
303
+ await generateExcel(data, 'Reporte', dispatch);
304
+ ```
305
+
306
+ ### Ahora (FileDownloadManager)
307
+ ```javascript
308
+ import { downloadExcel } from 'services/FileDownloadManager/adapter';
309
+
310
+ await downloadExcel(data, 'Reporte', dispatch);
311
+ ```
312
+
313
+ ### Antes (DownloadFileService.js para PDF)
314
+ ```javascript
315
+ import { download, downloadBase64, openDocument } from 'services/DownloadFileService';
316
+
317
+ const path = await download(base64Data, fileName, downloadBase64);
318
+ await openDocument(path);
319
+ ```
320
+
321
+ ### Ahora (FileDownloadManager)
322
+ ```javascript
323
+ import { downloadPDF } from 'services/FileDownloadManager/adapter';
324
+
325
+ await downloadPDF(base64Data, fileName, dispatch);
326
+ // Auto-abre con visor nativo
327
+ ```
328
+
329
+ ## 🛠️ Configuración Requerida
330
+
331
+ ### App.tsx
332
+
333
+ ```typescript
334
+ import * as Notifications from 'expo-notifications';
335
+
336
+ Notifications.setNotificationHandler({
337
+ handleNotification: async () => ({
338
+ shouldPlaySound: false,
339
+ shouldSetBadge: false,
340
+ shouldShowBanner: true,
341
+ shouldShowList: true
342
+ })
343
+ });
344
+ ```
345
+
346
+ ### AndroidManifest.xml
347
+
348
+ ```xml
349
+ <queries>
350
+ <!-- PDF -->
351
+ <intent>
352
+ <action android:name="android.intent.action.VIEW" />
353
+ <data android:mimeType="application/pdf" />
354
+ </intent>
355
+
356
+ <!-- Excel -->
357
+ <intent>
358
+ <action android:name="android.intent.action.VIEW" />
359
+ <data android:mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" />
360
+ </intent>
361
+
362
+ <!-- JSON -->
363
+ <intent>
364
+ <action android:name="android.intent.action.VIEW" />
365
+ <data android:mimeType="application/json" />
366
+ </intent>
367
+ </queries>
368
+
369
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
370
+ ```
371
+
372
+ ## 📦 Dependencias
373
+
374
+ ```json
375
+ {
376
+ "expo-file-system": "^19.0.17",
377
+ "expo-notifications": "^0.32.12",
378
+ "expo-sharing": "^14.0.7",
379
+ "react-native-file-viewer": "^2.1.5",
380
+ "xlsx": "^0.18.5",
381
+ "dayjs": "^1.11.x",
382
+ "base-64": "^1.0.0"
383
+ }
384
+ ```
385
+
386
+ ## 🎨 Personalización para @widergy/mobile-ui
387
+
388
+ El módulo core (`index.js`) está diseñado para ser portable. Para migrarlo a `@widergy/mobile-ui`:
389
+
390
+ 1. **Copiar `index.js` y `types.js`** al paquete
391
+ 2. **Exportar** desde el index principal
392
+ 3. **Documentar** que los consumidores deben pasar `showSnackbar` en options
393
+ 4. **Ejemplo de uso** en el README del paquete:
394
+
395
+ ```javascript
396
+ import { downloadFile, FILE_TYPES } from '@widergy/mobile-ui';
397
+
398
+ // El consumidor maneja sus propias notificaciones
399
+ const showSnackbar = (dispatch, type, error) => {
400
+ if (type === 'success') {
401
+ dispatch(showSuccess('Archivo descargado'));
402
+ } else {
403
+ dispatch(showError(error.message));
404
+ }
405
+ };
406
+
407
+ await downloadFile({
408
+ type: FILE_TYPES.PDF,
409
+ fileName: 'documento',
410
+ data: base64Content,
411
+ dispatch,
412
+ options: {
413
+ showSnackbar // Inyección de dependencia
414
+ }
415
+ });
416
+ ```
417
+
418
+ ## 🐛 Troubleshooting
419
+
420
+ ### "Unable to open URL: file://..."
421
+ - **Causa**: iOS no puede abrir archivos file:// directamente
422
+ - **Solución**: El módulo automáticamente usa Expo Sharing como fallback
423
+
424
+ ### "Expo FileSystem legacy no disponible"
425
+ - **Causa**: Falta dependencia o versión incorrecta
426
+ - **Solución**: `yarn add expo-file-system@^19.0.17`
427
+
428
+ ### Notificación se muestra pero no abre archivo
429
+ - **Causa**: Listener de notificaciones no registrado
430
+ - **Solución**: Verificar que `App.tsx` tenga configurado el `NotificationHandler`
431
+
432
+ ### PDF no se abre nativamente en Android
433
+ - **Causa**: Falta query en AndroidManifest
434
+ - **Solución**: Agregar los `<queries>` indicados arriba
435
+
436
+ ## 📝 TODO / Roadmap
437
+
438
+ - [ ] Soporte para más formatos (DOC, CSV, etc.)
439
+ - [ ] Progress callbacks para archivos grandes
440
+ - [ ] Compresión opcional
441
+ - [ ] Cache de archivos descargados
442
+ - [ ] Tests unitarios
443
+ - [ ] Integración con analytics
444
+
445
+ ## 📄 Licencia
446
+
447
+ Parte del proyecto UtilityGO - Widergy
@@ -0,0 +1,118 @@
1
+ // FileDownloadManager - Project adapter
2
+ // Adapts the generic FileDownloadManager to work with this project's specific Redux patterns
3
+
4
+ import { FILE_TYPES } from './types';
5
+
6
+ import { downloadFile as coreDownloadFile, openFile } from './index';
7
+
8
+ /**
9
+ * Show snackbar helper factory
10
+ * @param {Object} snackbarActions - Snackbar actions from the project
11
+ * @param {Object} translations - Translations object
12
+ */
13
+ const createShowSnackbar =
14
+ (snackbarActions, translations) =>
15
+ (dispatch, type, error = null) => {
16
+ if (type === 'success') {
17
+ dispatch(snackbarActions.displayInfo(translations.successDownload));
18
+ } else if (type === 'error') {
19
+ dispatch(snackbarActions.displayError(error?.message || translations.errorDownload));
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Download Excel file from data array
25
+ * @param {Array} data - Array of objects to export
26
+ * @param {string} fileName - Base file name
27
+ * @param {Function} dispatch - Redux dispatch
28
+ * @param {Object} snackbarActions - Snackbar actions from the project
29
+ * @param {Object} translations - Custom translations (optional)
30
+ * @param {Object} options - Additional options
31
+ */
32
+ export const downloadExcel = async (
33
+ data,
34
+ fileName,
35
+ dispatch,
36
+ snackbarActions,
37
+ translations = {},
38
+ options = {}
39
+ ) =>
40
+ coreDownloadFile({
41
+ type: FILE_TYPES.XLS,
42
+ fileName,
43
+ data,
44
+ dispatch,
45
+ translations,
46
+ options: {
47
+ ...options,
48
+ showSnackbar: createShowSnackbar(snackbarActions, translations)
49
+ }
50
+ });
51
+
52
+ /**
53
+ * Download PDF from base64
54
+ * @param {string} base64 - PDF content in base64
55
+ * @param {string} fileName - Base file name
56
+ * @param {Function} dispatch - Redux dispatch
57
+ * @param {Object} snackbarActions - Snackbar actions from the project
58
+ * @param {Object} translations - Custom translations (optional)
59
+ * @param {Object} options - Additional options
60
+ */
61
+ export const downloadPDF = async (
62
+ base64,
63
+ fileName,
64
+ dispatch,
65
+ snackbarActions,
66
+ translations = {},
67
+ options = {}
68
+ ) =>
69
+ coreDownloadFile({
70
+ type: FILE_TYPES.PDF,
71
+ fileName,
72
+ data: base64,
73
+ dispatch,
74
+ translations,
75
+ options: {
76
+ ...options,
77
+ showSnackbar: createShowSnackbar(snackbarActions, translations)
78
+ }
79
+ });
80
+
81
+ /**
82
+ * Download JSON file from object
83
+ * @param {Object|Array} data - JSON data to export
84
+ * @param {string} fileName - Base file name
85
+ * @param {Function} dispatch - Redux dispatch
86
+ * @param {Object} snackbarActions - Snackbar actions from the project
87
+ * @param {Object} translations - Custom translations (optional)
88
+ * @param {Object} options - Additional options
89
+ */
90
+ export const downloadJSON = async (
91
+ data,
92
+ fileName,
93
+ dispatch,
94
+ snackbarActions,
95
+ translations = {},
96
+ options = {}
97
+ ) =>
98
+ coreDownloadFile({
99
+ type: FILE_TYPES.JSON,
100
+ fileName,
101
+ data,
102
+ dispatch,
103
+ translations,
104
+ options: {
105
+ ...options,
106
+ showSnackbar: createShowSnackbar(snackbarActions, translations)
107
+ }
108
+ });
109
+
110
+ export { openFile, FILE_TYPES };
111
+
112
+ export default {
113
+ downloadExcel,
114
+ downloadPDF,
115
+ downloadJSON,
116
+ openFile,
117
+ FILE_TYPES
118
+ };