@widergy/mobile-ui 2.1.0 → 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.
- package/lib/components/FileDownloadManager/README.md +447 -0
- package/lib/components/FileDownloadManager/adapter.js +118 -0
- package/lib/components/FileDownloadManager/index.js +373 -0
- package/lib/components/FileDownloadManager/types.js +81 -0
- package/lib/components/FilePicker/index.js +10 -2
- package/lib/components/ImagePicker/index.js +8 -1
- package/lib/components/MultipleFilePicker/index.js +9 -2
- package/lib/components/UTTopbar/index.js +10 -12
- package/package.json +9 -2
- package/CHANGELOG.md +0 -2235
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/* eslint-disable import/no-unresolved */
|
|
2
|
+
// FileDownloadManager - Core file operations module
|
|
3
|
+
// Handles file generation, storage, notifications, and opening across platforms
|
|
4
|
+
|
|
5
|
+
import { File, Paths } from 'expo-file-system';
|
|
6
|
+
import * as FileSystemLegacy from 'expo-file-system/legacy';
|
|
7
|
+
import * as Notifications from 'expo-notifications';
|
|
8
|
+
import * as Sharing from 'expo-sharing';
|
|
9
|
+
import { Platform, InteractionManager } from 'react-native';
|
|
10
|
+
import { decode as atob, encode as btoa } from 'base-64';
|
|
11
|
+
import XLSX from 'xlsx';
|
|
12
|
+
import dayjs from 'dayjs';
|
|
13
|
+
import * as FileViewer from 'react-native-file-viewer';
|
|
14
|
+
|
|
15
|
+
import { FILE_TYPES, FILE_MIME_TYPES, VIEWER_SUPPORT, DEFAULT_TRANSLATIONS, replaceParams } from './types';
|
|
16
|
+
|
|
17
|
+
const DOWNLOAD_NOTIFICATION_CHANNEL_ID = 'file-downloads';
|
|
18
|
+
|
|
19
|
+
let notificationResponseListenerRegistered = false;
|
|
20
|
+
let androidNotificationChannelConfigured = false;
|
|
21
|
+
let cachedNotificationsPermission = null;
|
|
22
|
+
|
|
23
|
+
// ==================== Utilities ====================
|
|
24
|
+
|
|
25
|
+
const slugify = name =>
|
|
26
|
+
(name || 'document')
|
|
27
|
+
.toString()
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/[\s]+/g, '_')
|
|
30
|
+
.replace(/[^\w.-]+/g, '');
|
|
31
|
+
|
|
32
|
+
const buildTimestampSuffix = () => dayjs().format('DD_MM_YYYY_HH_mm_ss');
|
|
33
|
+
|
|
34
|
+
const appendTimestampToName = fileName => {
|
|
35
|
+
const lastDot = fileName.lastIndexOf('.');
|
|
36
|
+
const suffix = buildTimestampSuffix();
|
|
37
|
+
|
|
38
|
+
if (lastDot === -1) {
|
|
39
|
+
return `${fileName}_${suffix}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const base = fileName.substring(0, lastDot);
|
|
43
|
+
const extension = fileName.substring(lastDot);
|
|
44
|
+
return `${base}_${suffix}${extension}`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const base64ToUint8Array = base64 => {
|
|
48
|
+
const binary = atob(base64);
|
|
49
|
+
const bytes = new Uint8Array(binary.length);
|
|
50
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
51
|
+
bytes[i] = binary.charCodeAt(i);
|
|
52
|
+
}
|
|
53
|
+
return bytes;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const uint8ArrayToBase64 = bytes => {
|
|
57
|
+
if (!bytes) return '';
|
|
58
|
+
let binary = '';
|
|
59
|
+
const chunk = 0x8000;
|
|
60
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
61
|
+
const slice = bytes.subarray(i, i + chunk);
|
|
62
|
+
binary += String.fromCharCode.apply(null, Array.from(slice));
|
|
63
|
+
}
|
|
64
|
+
return btoa(binary);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const ensureContentUri = async uri => {
|
|
68
|
+
if (!uri) return null;
|
|
69
|
+
if (uri.startsWith('content://')) return uri;
|
|
70
|
+
|
|
71
|
+
const getContentUriAsync =
|
|
72
|
+
Platform.OS === 'android' && typeof FileSystemLegacy.getContentUriAsync === 'function'
|
|
73
|
+
? FileSystemLegacy.getContentUriAsync
|
|
74
|
+
: null;
|
|
75
|
+
|
|
76
|
+
if (!getContentUriAsync) {
|
|
77
|
+
throw new Error('Expo FileSystem legacy no disponible para obtener un content:// URI.');
|
|
78
|
+
}
|
|
79
|
+
return getContentUriAsync(uri);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ==================== File Generation ====================
|
|
83
|
+
|
|
84
|
+
const generateExcelFile = data => {
|
|
85
|
+
const ws = XLSX.utils.json_to_sheet(data);
|
|
86
|
+
const wb = XLSX.utils.book_new();
|
|
87
|
+
XLSX.utils.book_append_sheet(wb, ws, 'Datos');
|
|
88
|
+
return XLSX.write(wb, { type: 'base64', bookType: 'xlsx' });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const generateJsonFile = data => {
|
|
92
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
93
|
+
return btoa(jsonString);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const generateFileContent = (type, data) => {
|
|
97
|
+
switch (type) {
|
|
98
|
+
case FILE_TYPES.XLS:
|
|
99
|
+
return generateExcelFile(data);
|
|
100
|
+
case FILE_TYPES.JSON:
|
|
101
|
+
return generateJsonFile(data);
|
|
102
|
+
case FILE_TYPES.PDF:
|
|
103
|
+
// Para PDF esperamos que data ya sea base64
|
|
104
|
+
return data;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unsupported file type: ${type}`);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ==================== File Storage ====================
|
|
111
|
+
|
|
112
|
+
const saveToInternalDocuments = async (base64Data, fullFileName) => {
|
|
113
|
+
if (Platform.OS === 'ios') {
|
|
114
|
+
const base64Payload = typeof base64Data === 'string' ? base64Data : uint8ArrayToBase64(base64Data);
|
|
115
|
+
|
|
116
|
+
let targetName = fullFileName;
|
|
117
|
+
let targetUri = `${FileSystemLegacy.documentDirectory}${targetName}`;
|
|
118
|
+
const existingInfo = await FileSystemLegacy.getInfoAsync(targetUri);
|
|
119
|
+
|
|
120
|
+
if (existingInfo.exists) {
|
|
121
|
+
targetName = appendTimestampToName(fullFileName);
|
|
122
|
+
targetUri = `${FileSystemLegacy.documentDirectory}${targetName}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await FileSystemLegacy.writeAsStringAsync(targetUri, base64Payload, {
|
|
126
|
+
encoding: FileSystemLegacy.EncodingType.Base64
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const info = await FileSystemLegacy.getInfoAsync(targetUri);
|
|
130
|
+
if (!info?.exists || !info.uri) throw new Error('write failed');
|
|
131
|
+
|
|
132
|
+
return { fileName: targetName, fileUri: info.uri };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let targetName = fullFileName;
|
|
136
|
+
let file = new File(Paths.document, targetName);
|
|
137
|
+
const existingInfo = await file.info();
|
|
138
|
+
|
|
139
|
+
if (existingInfo.exists) {
|
|
140
|
+
targetName = appendTimestampToName(fullFileName);
|
|
141
|
+
file = new File(Paths.document, targetName);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await file.parentDirectory.create({ intermediates: true, idempotent: true });
|
|
145
|
+
await file.create({ overwrite: true });
|
|
146
|
+
const bytes = base64Data instanceof Uint8Array ? base64Data : base64ToUint8Array(base64Data);
|
|
147
|
+
await file.write(bytes);
|
|
148
|
+
|
|
149
|
+
const info = await file.info();
|
|
150
|
+
if (!info?.exists || !info.uri) throw new Error('write failed');
|
|
151
|
+
|
|
152
|
+
return { fileName: targetName, fileUri: info.uri };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ==================== File Opening ====================
|
|
156
|
+
|
|
157
|
+
const openFileWithNativeViewer = async uri => {
|
|
158
|
+
const viewerTarget = uri.startsWith('file://') ? uri.replace('file://', '') : uri;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await FileViewer.open(viewerTarget, {
|
|
162
|
+
showOpenWithDialog: Platform.OS === 'android'
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.log('Native viewer error:', error);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const shareFile = async (uri, fileName, fileType, translations) => {
|
|
173
|
+
try {
|
|
174
|
+
const isAvailable = await Sharing.isAvailableAsync();
|
|
175
|
+
if (!isAvailable) {
|
|
176
|
+
throw new Error('Sharing is not available on this device');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// En Android necesitamos content URI
|
|
180
|
+
const shareUri = Platform.OS === 'android' ? await ensureContentUri(uri) : uri;
|
|
181
|
+
|
|
182
|
+
await Sharing.shareAsync(shareUri, {
|
|
183
|
+
mimeType: FILE_MIME_TYPES[fileType],
|
|
184
|
+
dialogTitle: replaceParams(translations.shareFileTitle, { fileName }),
|
|
185
|
+
UTI: FILE_MIME_TYPES[fileType]
|
|
186
|
+
});
|
|
187
|
+
return true;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.log('Share error:', error);
|
|
191
|
+
throw new Error(translations.shareError);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const openOrShareFile = async (uri, fileName, fileType, translations) => {
|
|
196
|
+
if (!uri) throw new Error('No se pudo obtener la ubicación del archivo');
|
|
197
|
+
|
|
198
|
+
const supportsNativeViewer = VIEWER_SUPPORT[fileType];
|
|
199
|
+
|
|
200
|
+
// Intentar abrir con visor nativo si está soportado
|
|
201
|
+
if (supportsNativeViewer) {
|
|
202
|
+
const opened = await openFileWithNativeViewer(uri, fileType);
|
|
203
|
+
if (opened) return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Si no se pudo abrir nativamente o no está soportado, compartir
|
|
207
|
+
await shareFile(uri, fileName, fileType, translations);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// ==================== Notifications ====================
|
|
211
|
+
|
|
212
|
+
const ensureNotificationsPermissionAsync = async () => {
|
|
213
|
+
if (cachedNotificationsPermission === true) return true;
|
|
214
|
+
if (cachedNotificationsPermission === false) return false;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
let permissions = await Notifications.getPermissionsAsync();
|
|
218
|
+
if (!permissions.granted) {
|
|
219
|
+
permissions = await Notifications.requestPermissionsAsync();
|
|
220
|
+
}
|
|
221
|
+
cachedNotificationsPermission = Boolean(permissions.granted);
|
|
222
|
+
return cachedNotificationsPermission;
|
|
223
|
+
} catch (permissionError) {
|
|
224
|
+
// eslint-disable-next-line no-console
|
|
225
|
+
console.log('notifications permission error:', permissionError);
|
|
226
|
+
cachedNotificationsPermission = false;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const ensureAndroidNotificationChannelAsync = async () => {
|
|
232
|
+
if (Platform.OS !== 'android' || androidNotificationChannelConfigured) return;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await Notifications.setNotificationChannelAsync(DOWNLOAD_NOTIFICATION_CHANNEL_ID, {
|
|
236
|
+
name: 'File Downloads',
|
|
237
|
+
importance: Notifications.AndroidImportance.DEFAULT
|
|
238
|
+
});
|
|
239
|
+
androidNotificationChannelConfigured = true;
|
|
240
|
+
} catch (channelError) {
|
|
241
|
+
// eslint-disable-next-line no-console
|
|
242
|
+
console.log('notification channel error:', channelError);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const ensureNotificationListener = translations => {
|
|
247
|
+
if (notificationResponseListenerRegistered) return;
|
|
248
|
+
|
|
249
|
+
Notifications.addNotificationResponseReceivedListener(response => {
|
|
250
|
+
const { fileUri, fileName, fileType } = response.notification.request.content.data || {};
|
|
251
|
+
if (!fileUri) return;
|
|
252
|
+
|
|
253
|
+
InteractionManager.runAfterInteractions(() => {
|
|
254
|
+
openOrShareFile(fileUri, fileName, fileType, translations).catch(openError => {
|
|
255
|
+
// eslint-disable-next-line no-console
|
|
256
|
+
console.log('notification response open error:', openError);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
notificationResponseListenerRegistered = true;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const notifyDownloadSuccess = async (fileName, fileUri, fileType, translations) => {
|
|
265
|
+
if (!fileUri) return;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
ensureNotificationListener(translations);
|
|
269
|
+
const hasPermission = await ensureNotificationsPermissionAsync();
|
|
270
|
+
if (!hasPermission) return;
|
|
271
|
+
|
|
272
|
+
if (Platform.OS === 'android') {
|
|
273
|
+
await ensureAndroidNotificationChannelAsync();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await Notifications.scheduleNotificationAsync({
|
|
277
|
+
content: {
|
|
278
|
+
title: translations.downloadReadyTitle,
|
|
279
|
+
body: replaceParams(translations.downloadReadyBody, { fileName }),
|
|
280
|
+
data: { fileUri, fileName, fileType },
|
|
281
|
+
...(Platform.OS === 'android' ? { channelId: DOWNLOAD_NOTIFICATION_CHANNEL_ID } : {})
|
|
282
|
+
},
|
|
283
|
+
trigger: null
|
|
284
|
+
});
|
|
285
|
+
} catch (notificationError) {
|
|
286
|
+
// eslint-disable-next-line no-console
|
|
287
|
+
console.log('notifyDownloadSuccess error:', notificationError);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ==================== Public API ====================
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Main function to handle file download workflow
|
|
295
|
+
* @param {FileGenerationConfig} config - File generation configuration
|
|
296
|
+
* @returns {Promise<{success: boolean, fileUri: string, fileName: string}>}
|
|
297
|
+
*/
|
|
298
|
+
export const downloadFile = async config => {
|
|
299
|
+
const { type, fileName, data, dispatch, translations: customTranslations, options = {} } = config;
|
|
300
|
+
|
|
301
|
+
// Merge custom translations with defaults
|
|
302
|
+
const translations = { ...DEFAULT_TRANSLATIONS, ...customTranslations };
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// 1. Generate file content
|
|
306
|
+
const base64Content = generateFileContent(type, data);
|
|
307
|
+
|
|
308
|
+
// 2. Save to internal storage
|
|
309
|
+
const safeName = `${slugify(fileName)}.${type}`;
|
|
310
|
+
const { fileUri, fileName: storedFileName } = await saveToInternalDocuments(base64Content, safeName);
|
|
311
|
+
|
|
312
|
+
// 3. Show success notification via dispatch (if provided)
|
|
313
|
+
if (dispatch && !options.hideNotification && options.showSnackbar) {
|
|
314
|
+
options.showSnackbar(dispatch, 'success');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 4. Send notification
|
|
318
|
+
if (!options.hideNotification) {
|
|
319
|
+
await notifyDownloadSuccess(storedFileName, fileUri, type, translations);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 5. Auto-open if requested
|
|
323
|
+
if (!options.preventAutoOpen) {
|
|
324
|
+
await openOrShareFile(fileUri, storedFileName, type, translations);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 6. Success callback
|
|
328
|
+
if (options.successCallback) {
|
|
329
|
+
options.successCallback({ fileUri, fileName: storedFileName });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
fileUri,
|
|
335
|
+
fileName: storedFileName
|
|
336
|
+
};
|
|
337
|
+
} catch (error) {
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.log('downloadFile error:', error);
|
|
340
|
+
|
|
341
|
+
// Error callback
|
|
342
|
+
if (options.failureCallback) {
|
|
343
|
+
options.failureCallback(error);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Show error via dispatch
|
|
347
|
+
if (dispatch && options.showSnackbar) {
|
|
348
|
+
options.showSnackbar(dispatch, 'error', error);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Open a previously saved file
|
|
357
|
+
* @param {string} fileUri - File URI
|
|
358
|
+
* @param {string} fileName - File name
|
|
359
|
+
* @param {string} fileType - File type
|
|
360
|
+
* @param {Translations} customTranslations - Custom translations (optional)
|
|
361
|
+
*/
|
|
362
|
+
export const openFile = async (fileUri, fileName, fileType, customTranslations = {}) => {
|
|
363
|
+
const translations = { ...DEFAULT_TRANSLATIONS, ...customTranslations };
|
|
364
|
+
await openOrShareFile(fileUri, fileName, fileType, translations);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
downloadFile,
|
|
369
|
+
openFile,
|
|
370
|
+
FILE_TYPES,
|
|
371
|
+
FILE_MIME_TYPES,
|
|
372
|
+
VIEWER_SUPPORT
|
|
373
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// FileDownloadManager types and constants
|
|
2
|
+
|
|
3
|
+
export const FILE_TYPES = {
|
|
4
|
+
PDF: 'pdf',
|
|
5
|
+
XLS: 'xlsx',
|
|
6
|
+
JSON: 'json'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const FILE_MIME_TYPES = {
|
|
10
|
+
[FILE_TYPES.PDF]: 'application/pdf',
|
|
11
|
+
[FILE_TYPES.XLS]: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
12
|
+
[FILE_TYPES.JSON]: 'application/json'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const VIEWER_SUPPORT = {
|
|
16
|
+
[FILE_TYPES.PDF]: true, // Native PDF viewer
|
|
17
|
+
[FILE_TYPES.XLS]: false, // Share only
|
|
18
|
+
[FILE_TYPES.JSON]: false // Share only
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} Translations
|
|
23
|
+
* @property {string} downloadReadyTitle - Título de la notificación cuando el archivo está listo
|
|
24
|
+
* Ejemplo: "Archivo listo" o "File ready"
|
|
25
|
+
*
|
|
26
|
+
* @property {string} downloadReadyBody - Cuerpo de la notificación con el nombre del archivo
|
|
27
|
+
* Parámetros: {fileName}
|
|
28
|
+
* Ejemplo: "{fileName} está disponible" o "{fileName} is available"
|
|
29
|
+
*
|
|
30
|
+
* @property {string} shareFileTitle - Título del diálogo de compartir archivo
|
|
31
|
+
* Parámetros: {fileName}
|
|
32
|
+
* Ejemplo: "Compartir {fileName}" o "Share {fileName}"
|
|
33
|
+
*
|
|
34
|
+
* @property {string} shareError - Mensaje de error cuando falla el compartir
|
|
35
|
+
* Ejemplo: "No se pudo compartir el archivo" o "Could not share file"
|
|
36
|
+
*
|
|
37
|
+
* @property {string} successDownload - Mensaje de éxito para snackbar/toast
|
|
38
|
+
* Ejemplo: "Archivo descargado correctamente" o "File downloaded successfully"
|
|
39
|
+
*
|
|
40
|
+
* @property {string} errorDownload - Mensaje de error para snackbar/toast
|
|
41
|
+
* Ejemplo: "Error al descargar el archivo" o "Error downloading file"
|
|
42
|
+
*//**
|
|
43
|
+
* Traducciones por defecto en español
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_TRANSLATIONS = {
|
|
46
|
+
downloadReadyTitle: 'Archivo listo',
|
|
47
|
+
downloadReadyBody: '{fileName} está disponible',
|
|
48
|
+
shareFileTitle: 'Compartir {fileName}',
|
|
49
|
+
shareError: 'No se pudo compartir el archivo',
|
|
50
|
+
successDownload: 'Archivo descargado correctamente',
|
|
51
|
+
errorDownload: 'Error al descargar el archivo'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reemplaza parámetros en un string de traducción
|
|
56
|
+
* @param {string} template - Template con parámetros entre llaves
|
|
57
|
+
* @param {Object} params - Objeto con los valores a reemplazar
|
|
58
|
+
* @returns {string} String con parámetros reemplazados
|
|
59
|
+
*/
|
|
60
|
+
export const replaceParams = (template, params = {}) => {
|
|
61
|
+
if (!template) return '';
|
|
62
|
+
return Object.keys(params).reduce(
|
|
63
|
+
(result, key) => result.replace(new RegExp(`\\{${key}\\}`, 'g'), params[key]),
|
|
64
|
+
template
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {Object} FileGenerationConfig
|
|
70
|
+
* @property {string} type - File type: 'pdf', 'xlsx', 'json'
|
|
71
|
+
* @property {string} fileName - Base name without extension
|
|
72
|
+
* @property {any} data - File data (Excel data array, JSON object, or base64 for PDF)
|
|
73
|
+
* @property {Function} [dispatch] - Redux dispatch function for notifications
|
|
74
|
+
* @property {Translations} [translations] - Custom translations (falls back to DEFAULT_TRANSLATIONS)
|
|
75
|
+
* @property {Object} [options] - Additional options
|
|
76
|
+
* @property {boolean} [options.hideNotification] - Skip notification
|
|
77
|
+
* @property {boolean} [options.preventAutoOpen] - Don't auto-open after save
|
|
78
|
+
* @property {Function} [options.successCallback] - Called after successful save
|
|
79
|
+
* @property {Function} [options.failureCallback] - Called on error
|
|
80
|
+
* @property {Function} [options.showSnackbar] - Custom snackbar function
|
|
81
|
+
*/
|
|
@@ -31,6 +31,13 @@ class FilePicker extends Component {
|
|
|
31
31
|
allowedPDFUploadSizes,
|
|
32
32
|
pdfFormatError
|
|
33
33
|
} = this.props;
|
|
34
|
+
|
|
35
|
+
const getRealFileType = mimeType => {
|
|
36
|
+
if (!mimeType || typeof mimeType !== 'string') return null;
|
|
37
|
+
const parts = mimeType.split('/');
|
|
38
|
+
return parts[1]?.toLowerCase() || null;
|
|
39
|
+
};
|
|
40
|
+
|
|
34
41
|
try {
|
|
35
42
|
const normalizeTypes = types => {
|
|
36
43
|
if (!types || types.length === 0) return DEFAULT_ALLOWED_TYPES;
|
|
@@ -71,7 +78,8 @@ class FilePicker extends Component {
|
|
|
71
78
|
onMaxSizeError(document.size, maxFileByteSize);
|
|
72
79
|
return;
|
|
73
80
|
}
|
|
74
|
-
const
|
|
81
|
+
const realFileType = getRealFileType(document.type);
|
|
82
|
+
const file = !avoidRetrieveFile && (await retrieveFile(document.uri, realFileType));
|
|
75
83
|
|
|
76
84
|
if (file && isPDF && !isEmpty(allowedPDFUploadSizes)) {
|
|
77
85
|
const isWrongFormat = await pdfAspectRatioValidation(file, allowedPDFUploadSizes);
|
|
@@ -85,7 +93,7 @@ class FilePicker extends Component {
|
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
95
|
if (onChange) {
|
|
88
|
-
onChange(avoidRetrieveFile ? { document } : { file: blobToFile(file,
|
|
96
|
+
onChange(avoidRetrieveFile ? { document } : { file: blobToFile(file, realFileType) });
|
|
89
97
|
}
|
|
90
98
|
this.setState({ fileName: document.name });
|
|
91
99
|
} catch (err) {
|
|
@@ -57,7 +57,14 @@ const ImagePickerComponent = ({
|
|
|
57
57
|
onError('No se pudo obtener el archivo');
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
|
-
const
|
|
60
|
+
const getRealFileType = mimeType => {
|
|
61
|
+
if (!mimeType || typeof mimeType !== 'string') return null;
|
|
62
|
+
const parts = mimeType.split('/');
|
|
63
|
+
return parts[1]?.toLowerCase() || null;
|
|
64
|
+
};
|
|
65
|
+
const realFileType = getRealFileType(item.type);
|
|
66
|
+
|
|
67
|
+
const file = !avoidRetrieveFile && (await retrieveFile(item.uri, realFileType));
|
|
61
68
|
if (!avoidRetrieveFile && !file) {
|
|
62
69
|
onError(response.errorCode);
|
|
63
70
|
return;
|
|
@@ -79,6 +79,12 @@ const MultipleFilePicker = ({
|
|
|
79
79
|
|
|
80
80
|
const remainingSlots = () => Math.max((maxFiles || 0) - uploadedFiles.length, 0);
|
|
81
81
|
|
|
82
|
+
const getRealFileType = mimeType => {
|
|
83
|
+
if (!mimeType || typeof mimeType !== 'string') return null;
|
|
84
|
+
const parts = mimeType.split('/');
|
|
85
|
+
return parts[1]?.toLowerCase() || null;
|
|
86
|
+
};
|
|
87
|
+
|
|
82
88
|
const handleAssets = async response => {
|
|
83
89
|
if (!response || response.didCancel) {
|
|
84
90
|
return;
|
|
@@ -89,7 +95,8 @@ const MultipleFilePicker = ({
|
|
|
89
95
|
}
|
|
90
96
|
closeDrawer();
|
|
91
97
|
response.assets.forEach(async asset => {
|
|
92
|
-
const
|
|
98
|
+
const realFileType = getRealFileType(asset.mimeType);
|
|
99
|
+
const file = await retrieveFile(asset.uri, realFileType);
|
|
93
100
|
if (!file) {
|
|
94
101
|
onError(response.errorCode || 'No se pudo obtener el archivo');
|
|
95
102
|
return;
|
|
@@ -98,7 +105,7 @@ const MultipleFilePicker = ({
|
|
|
98
105
|
if (isFileSizeInvaid({ size: file.size }, maxFileByteSize, onMaxSizeError)) return;
|
|
99
106
|
setNewFile({
|
|
100
107
|
uploadFile: { name: file.data.name, size: file.data.size },
|
|
101
|
-
rawFile: blobToFile(file,
|
|
108
|
+
rawFile: blobToFile(file, realFileType)
|
|
102
109
|
});
|
|
103
110
|
});
|
|
104
111
|
};
|
|
@@ -20,7 +20,7 @@ const UTTopbar = ({
|
|
|
20
20
|
stages,
|
|
21
21
|
stepsCount,
|
|
22
22
|
theme,
|
|
23
|
-
topbar: { colorTheme = 'light', goBack, title, Icon
|
|
23
|
+
topbar: { colorTheme = 'light', goBack, title, Icon } = {}
|
|
24
24
|
}) => {
|
|
25
25
|
const ownTheme = theme.UTWorkflowContainer?.topbar?.[colorTheme];
|
|
26
26
|
|
|
@@ -41,17 +41,15 @@ const UTTopbar = ({
|
|
|
41
41
|
return (
|
|
42
42
|
<View>
|
|
43
43
|
<View style={[ownStyles.container, ownTheme?.container]}>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/>
|
|
54
|
-
)}
|
|
44
|
+
<UTButton
|
|
45
|
+
dataTestId={goBackTestId}
|
|
46
|
+
Icon={Icon || 'IconArrowLeft'}
|
|
47
|
+
onPress={goBack}
|
|
48
|
+
style={{
|
|
49
|
+
root: ownStyles.goBack
|
|
50
|
+
}}
|
|
51
|
+
variant="text"
|
|
52
|
+
/>
|
|
55
53
|
<UTLabel
|
|
56
54
|
colorTheme={ownTheme?.text || 'dark'}
|
|
57
55
|
dataTestId={titleTestId}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@widergy/mobile-ui",
|
|
3
3
|
"description": "Widergy Mobile Components",
|
|
4
4
|
"author": "widergy",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "3.4.1",
|
|
6
6
|
"repository": "https://github.com/widergy/mobile-ui.git",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"files": [
|
|
@@ -37,23 +37,30 @@
|
|
|
37
37
|
"@react-navigation/native": "^6.1.9",
|
|
38
38
|
"@tabler/icons-react-native": "^3.34.1",
|
|
39
39
|
"@widergy/web-utils": "^2.0.0",
|
|
40
|
+
"base-64": "^1.0.0",
|
|
40
41
|
"core-js": "3",
|
|
42
|
+
"dayjs": "^1.11.18",
|
|
41
43
|
"deprecated-react-native-prop-types": "^5.0.0",
|
|
42
44
|
"expo-document-picker": "^13.1.6",
|
|
45
|
+
"expo-file-system": "^19.0.17",
|
|
43
46
|
"expo-image-manipulator": "^13.1.7",
|
|
44
47
|
"expo-image-picker": "^16.1.4",
|
|
48
|
+
"expo-notifications": "^0.32.12",
|
|
49
|
+
"expo-sharing": "^14.0.7",
|
|
45
50
|
"invariant": "^2.2.4",
|
|
46
51
|
"lodash": "^4.17.21",
|
|
47
52
|
"numeral": "^2.0.6",
|
|
48
53
|
"pdf-lib": "^1.17.1",
|
|
49
54
|
"react-native-collapsible": "^1.6.1",
|
|
55
|
+
"react-native-file-viewer": "^2.1.5",
|
|
50
56
|
"react-native-linear-gradient": "^2.8.3",
|
|
51
57
|
"react-native-markdown-display": "^7.0.0-alpha.2",
|
|
52
58
|
"react-native-modal": "^13.0.2",
|
|
53
59
|
"react-native-pager-view": "^6.8.0",
|
|
54
60
|
"react-native-safe-area-context": "^5.2.0",
|
|
55
61
|
"react-native-svg": "^15.11.2",
|
|
56
|
-
"react-native-uuid": "^2.0.3"
|
|
62
|
+
"react-native-uuid": "^2.0.3",
|
|
63
|
+
"xlsx": "^0.18.5"
|
|
57
64
|
},
|
|
58
65
|
"devDependencies": {
|
|
59
66
|
"@babel/cli": "^7.22.10",
|