@ammarahmed/react-native-upload 6.16.0 → 6.18.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.
package/src/index.ts ADDED
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Handles HTTP background file uploads from an iOS or Android device.
3
+ */
4
+ import {
5
+ NativeModules,
6
+ DeviceEventEmitter,
7
+ Platform,
8
+ EmitterSubscription,
9
+ } from 'react-native';
10
+
11
+ export type UploadEvent = 'progress' | 'error' | 'completed' | 'cancelled';
12
+
13
+ export type NotificationOptions = {
14
+ /**
15
+ * Enable or diasable notifications. Works only on Android version < 8.0 Oreo. On Android versions >= 8.0 Oreo is required by Google's policy to display a notification when a background service run { enabled: true }
16
+ */
17
+ enabled: boolean;
18
+ /**
19
+ * Autoclear notification on complete { autoclear: true }
20
+ */
21
+ autoClear: boolean;
22
+ /**
23
+ * Sets android notificaion channel { notificationChannel: "My-Upload-Service" }
24
+ */
25
+ notificationChannel: string;
26
+ /**
27
+ * Sets whether or not to enable the notification sound when the upload gets completed with success or error { enableRingTone: true }
28
+ */
29
+ enableRingTone: boolean;
30
+ /**
31
+ * Sets notification progress title { onProgressTitle: "Uploading" }
32
+ */
33
+ onProgressTitle: string;
34
+ /**
35
+ * Sets notification progress message { onProgressMessage: "Uploading new video" }
36
+ */
37
+ onProgressMessage: string;
38
+ /**
39
+ * Sets notification complete title { onCompleteTitle: "Upload finished" }
40
+ */
41
+ onCompleteTitle: string;
42
+ /**
43
+ * Sets notification complete message { onCompleteMessage: "Your video has been uploaded" }
44
+ */
45
+ onCompleteMessage: string;
46
+ /**
47
+ * Sets notification error title { onErrorTitle: "Upload error" }
48
+ */
49
+ onErrorTitle: string;
50
+ /**
51
+ * Sets notification error message { onErrorMessage: "An error occured while uploading a video" }
52
+ */
53
+ onErrorMessage: string;
54
+ /**
55
+ * Sets notification cancelled title { onCancelledTitle: "Upload cancelled" }
56
+ */
57
+ onCancelledTitle: string;
58
+ /**
59
+ * Sets notification cancelled message { onCancelledMessage: "Video upload was cancelled" }
60
+ */
61
+ onCancelledMessage: string;
62
+ };
63
+
64
+ export type UploadOptions = {
65
+ url: string;
66
+ path: string;
67
+ method?: 'PUT' | 'POST';
68
+ type?: 'raw' | 'multipart';
69
+ field?: string;
70
+ /**
71
+ * Provide this id to track uploads globally and avoid duplicate upload tasks
72
+ */
73
+ customUploadId?: string;
74
+ parameters?: Record<string, string>;
75
+ headers?: Record<string, string>;
76
+ notification?: Partial<NotificationOptions>;
77
+ /**
78
+ * AppGroup defined in XCode for extensions. Necessary when trying to upload things via this library
79
+ * in the context of ShareExtension.
80
+ */
81
+ appGroup?: string;
82
+ };
83
+
84
+ export interface MultipartUploadOptions extends UploadOptions {
85
+ type: 'multipart';
86
+ field: string;
87
+ parameters?: {
88
+ [index: string]: string;
89
+ };
90
+ }
91
+
92
+ export interface FileInfo {
93
+ exists: boolean;
94
+ extension?: string;
95
+ size?: number;
96
+ mimeType?: string;
97
+ name?: string;
98
+ }
99
+
100
+ export interface ProgressData {
101
+ id: string;
102
+ progress: number;
103
+ totalBytes: number;
104
+ uploadedBytes: number;
105
+ }
106
+
107
+ export interface ErrorData {
108
+ id: string;
109
+ error: string;
110
+ }
111
+
112
+ export interface CancelledData {
113
+ id: string;
114
+ error: string;
115
+ }
116
+
117
+ export interface CompletedData {
118
+ id: string;
119
+ responseCode?: number;
120
+ responseBody?: string | null;
121
+ }
122
+
123
+ export type UploadEventData =
124
+ | ProgressData
125
+ | ErrorData
126
+ | CancelledData
127
+ | CompletedData;
128
+
129
+ const NativeModule =
130
+ NativeModules.VydiaRNFileUploader || NativeModules.RNFileUploader;
131
+ const eventPrefix = 'RNFileUploader-';
132
+
133
+ // for IOS, register event listeners or else they don't fire on DeviceEventEmitter
134
+ if (NativeModules.VydiaRNFileUploader) {
135
+ NativeModule.addListener(eventPrefix + 'progress');
136
+ NativeModule.addListener(eventPrefix + 'error');
137
+ NativeModule.addListener(eventPrefix + 'cancelled');
138
+ NativeModule.addListener(eventPrefix + 'completed');
139
+ }
140
+
141
+ export const UploadState = {
142
+ Cancelled: 'cancelled',
143
+ Completed: 'completed',
144
+ Pending: 'pending',
145
+ Running: 'running',
146
+ Error: 'error',
147
+ } as const;
148
+
149
+ export type UploadStatus = typeof UploadState[keyof typeof UploadState];
150
+
151
+ export interface UploadChangeEvent {
152
+ status: UploadStatus;
153
+ progress?: number;
154
+ error?: string;
155
+ responseCode?: number;
156
+ responseBody?: string | null;
157
+ }
158
+
159
+ export interface UploadResult {
160
+ status: UploadStatus;
161
+ error?: string;
162
+ responseCode?: number;
163
+ responseBody?: string | null;
164
+ }
165
+
166
+ export interface NativeUploadInfo {
167
+ id: string;
168
+ state: UploadStatus;
169
+ }
170
+
171
+ // Global registry to track active uploads and prevent duplicates
172
+ class UploadRegistry {
173
+ public static uploads: Map<string, Upload> = new Map();
174
+
175
+ static register(upload: Upload): void {
176
+ const id = upload.getId();
177
+ if (id) {
178
+ this.uploads.set(id, upload);
179
+ }
180
+ }
181
+
182
+ static unregister(upload: Upload): void {
183
+ const id = upload.getId();
184
+ if (id) {
185
+ this.uploads.delete(id);
186
+ }
187
+ }
188
+
189
+ static getById(id: string): Upload | undefined {
190
+ return this.uploads.get(id);
191
+ }
192
+
193
+ static has(id: string): boolean {
194
+ return this.uploads.has(id);
195
+ }
196
+
197
+ static clear(): void {
198
+ this.uploads.clear();
199
+ }
200
+ }
201
+
202
+ class Upload {
203
+ private uploadId: string | null = null;
204
+ private config: UploadOptions;
205
+ private subscriptions: EmitterSubscription[] = [];
206
+ private status: UploadStatus = UploadState.Pending;
207
+ private startPromise: Promise<UploadResult> | null = null;
208
+ private resolveStart: ((result: UploadResult) => void) | null = null;
209
+ private rejectStart: ((error: Error) => void) | null = null;
210
+ private changeCallback: ((event: UploadChangeEvent) => void) | null = null;
211
+
212
+ constructor(config: UploadOptions) {
213
+ this.config = config;
214
+ }
215
+
216
+ /**
217
+ * Create a new upload instance or return existing one for the same path
218
+ */
219
+ static create(config: UploadOptions | MultipartUploadOptions): Upload {
220
+ // Check if there's an existing upload for this upload id
221
+ const existingUpload = config.customUploadId
222
+ ? UploadRegistry.getById(config.customUploadId)
223
+ : null;
224
+ if (existingUpload && existingUpload.isRunning()) {
225
+ console.warn(
226
+ `Upload already in progress for path: ${
227
+ config.path
228
+ }. Returning existing upload.`,
229
+ );
230
+ return existingUpload;
231
+ }
232
+
233
+ const upload = new Upload(config);
234
+ return upload;
235
+ }
236
+
237
+ /**
238
+ * Resume an existing upload by ID (useful after app restart)
239
+ */
240
+ static async resume(uploadId: string): Promise<Upload | null> {
241
+ // Check if already tracked
242
+ const existingUpload = UploadRegistry.getById(uploadId);
243
+ if (existingUpload) {
244
+ return existingUpload;
245
+ }
246
+
247
+ // Get all uploads from native side
248
+ const nativeUploads = await getAllUploads();
249
+ const uploadInfo = nativeUploads.find(u => u.id === uploadId);
250
+
251
+ if (!uploadInfo) {
252
+ return null;
253
+ }
254
+
255
+ // Create a minimal upload instance for resumed upload
256
+ const upload = new Upload({ url: '', path: '' }); // We don't have the original config
257
+ upload.uploadId = uploadId;
258
+ upload.status = upload.mapNativeStateToStatus(uploadInfo.state);
259
+
260
+ // Register and setup listeners
261
+ UploadRegistry.register(upload);
262
+ upload.setupEventListeners();
263
+
264
+ return upload;
265
+ }
266
+
267
+ /**
268
+ * Get all currently tracked uploads
269
+ */
270
+ static getAll(): Upload[] {
271
+ const uploads: Upload[] = [];
272
+ UploadRegistry.uploads.forEach(upload => uploads.push(upload));
273
+ return uploads;
274
+ }
275
+
276
+ /**
277
+ * Set a callback to be called whenever the upload state changes
278
+ */
279
+ onChange(callback: (event: UploadChangeEvent) => void): this {
280
+ this.changeCallback = callback;
281
+ return this;
282
+ }
283
+
284
+ /**
285
+ * Start the upload - resolves when upload completes, is cancelled, or errors
286
+ */
287
+ async start(): Promise<UploadResult> {
288
+ if (this.uploadId) {
289
+ throw new Error('Upload already started');
290
+ }
291
+
292
+ if (this.startPromise) {
293
+ return this.startPromise;
294
+ }
295
+
296
+ // Check if there's an existing upload for this path in native side
297
+ if (this.config.path) {
298
+ const nativeUploads = await getAllUploads();
299
+ const existingUpload = nativeUploads.find(
300
+ u => u.state === 'running' || u.state === 'pending',
301
+ );
302
+
303
+ if (existingUpload && !this.config.customUploadId) {
304
+ console.warn(
305
+ `Found existing upload in native side. Resuming upload: ${
306
+ existingUpload.id
307
+ }`,
308
+ );
309
+ this.uploadId = existingUpload.id;
310
+ this.status = this.mapNativeStateToStatus(existingUpload.state);
311
+ UploadRegistry.register(this);
312
+ }
313
+ }
314
+
315
+ this.startPromise = new Promise<UploadResult>((resolve, reject) => {
316
+ this.resolveStart = resolve;
317
+ this.rejectStart = reject;
318
+ });
319
+
320
+ // Register event listeners
321
+ this.setupEventListeners();
322
+
323
+ // If we resumed an existing upload, don't call native startUpload again
324
+ if (!this.uploadId) {
325
+ try {
326
+ this.uploadId = await NativeModule.startUpload(this.config);
327
+ this.updateStatus(UploadState.Running);
328
+ UploadRegistry.register(this);
329
+ } catch (error) {
330
+ this.cleanup();
331
+ if (this.rejectStart) {
332
+ this.rejectStart(error as Error);
333
+ }
334
+ throw error;
335
+ }
336
+ }
337
+
338
+ return this.startPromise;
339
+ }
340
+
341
+ private setupEventListeners(): void {
342
+ // Progress listener
343
+ const progressSubscription = DeviceEventEmitter.addListener(
344
+ eventPrefix + 'progress',
345
+ (data: ProgressData) => {
346
+ if (this.uploadId && data.id === this.uploadId) {
347
+ this.notifyChange({
348
+ status: UploadState.Running,
349
+ progress: data.progress,
350
+ });
351
+ }
352
+ },
353
+ );
354
+ this.subscriptions.push(progressSubscription);
355
+
356
+ // Completed listener
357
+ const completedSubscription = DeviceEventEmitter.addListener(
358
+ eventPrefix + 'completed',
359
+ (data: CompletedData) => {
360
+ if (this.uploadId && data.id === this.uploadId) {
361
+ this.updateStatus(
362
+ UploadState.Completed,
363
+ undefined,
364
+ data.responseCode,
365
+ data.responseBody,
366
+ );
367
+ if (this.resolveStart) {
368
+ this.resolveStart({
369
+ status: 'completed',
370
+ responseCode: data.responseCode,
371
+ responseBody: data.responseBody,
372
+ });
373
+ }
374
+ this.cleanup();
375
+ }
376
+ },
377
+ );
378
+ this.subscriptions.push(completedSubscription);
379
+
380
+ // Error listener
381
+ const errorSubscription = DeviceEventEmitter.addListener(
382
+ eventPrefix + 'error',
383
+ (data: ErrorData) => {
384
+ if (this.uploadId && data.id === this.uploadId) {
385
+ this.updateStatus(UploadState.Error, data.error);
386
+ if (this.resolveStart) {
387
+ this.resolveStart({ status: 'error', error: data.error });
388
+ }
389
+ this.cleanup();
390
+ }
391
+ },
392
+ );
393
+ this.subscriptions.push(errorSubscription);
394
+
395
+ // Cancelled listener
396
+ const cancelledSubscription = DeviceEventEmitter.addListener(
397
+ eventPrefix + 'cancelled',
398
+ (data: CancelledData) => {
399
+ if (this.uploadId && data.id === this.uploadId) {
400
+ this.updateStatus(UploadState.Cancelled, data.error);
401
+ if (this.resolveStart) {
402
+ this.resolveStart({ status: 'cancelled', error: data.error });
403
+ }
404
+ this.cleanup();
405
+ }
406
+ },
407
+ );
408
+ this.subscriptions.push(cancelledSubscription);
409
+ }
410
+
411
+ private updateStatus(
412
+ status: UploadStatus,
413
+ error?: string,
414
+ responseCode?: number,
415
+ responseBody?: string | null,
416
+ ): void {
417
+ this.status = status;
418
+ this.notifyChange({ status, error, responseCode, responseBody });
419
+ }
420
+
421
+ private notifyChange(event: UploadChangeEvent): void {
422
+ if (this.changeCallback) {
423
+ this.changeCallback(event);
424
+ }
425
+ }
426
+
427
+ private mapNativeStateToStatus(state: string): UploadStatus {
428
+ switch (state) {
429
+ case 'running':
430
+ return UploadState.Running;
431
+ case 'pending':
432
+ return UploadState.Pending;
433
+ case 'cancelled':
434
+ return UploadState.Cancelled;
435
+ case 'completed':
436
+ return UploadState.Completed;
437
+ default:
438
+ return UploadState.Pending;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Cancel the upload
444
+ */
445
+ async cancel(): Promise<boolean> {
446
+ if (!this.uploadId) {
447
+ throw new Error('Upload not started');
448
+ }
449
+
450
+ try {
451
+ const result = await NativeModule.cancelUpload(this.uploadId);
452
+ // Don't cleanup here - let the cancelled event handle it
453
+ return result;
454
+ } catch (error) {
455
+ this.cleanup();
456
+ throw error;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Get the current upload status
462
+ */
463
+ getStatus(): UploadStatus {
464
+ return this.status;
465
+ }
466
+
467
+ /**
468
+ * Get the upload ID
469
+ */
470
+ getId(): string | null {
471
+ return this.uploadId;
472
+ }
473
+
474
+ /**
475
+ * Get the file path
476
+ */
477
+ getPath(): string {
478
+ return this.config.path;
479
+ }
480
+
481
+ /**
482
+ * Check if upload is in progress
483
+ */
484
+ isRunning(): boolean {
485
+ return this.status === UploadState.Running;
486
+ }
487
+
488
+ /**
489
+ * Clean up listeners
490
+ */
491
+ private cleanup(): void {
492
+ this.subscriptions.forEach(sub => sub.remove());
493
+ this.subscriptions = [];
494
+ this.resolveStart = null;
495
+ this.rejectStart = null;
496
+ UploadRegistry.unregister(this);
497
+ }
498
+ }
499
+
500
+ // Legacy API exports for backward compatibility
501
+ export const getFileInfo = (path: string): Promise<FileInfo> => {
502
+ return NativeModule.getFileInfo(path).then((data: FileInfo) => {
503
+ if (data.size) {
504
+ data.size = +data.size;
505
+ }
506
+ return data;
507
+ });
508
+ };
509
+
510
+ export const startUpload = (options: UploadOptions): Promise<string> =>
511
+ NativeModule.startUpload(options);
512
+
513
+ export const cancelUpload = (cancelUploadId: string): Promise<boolean> => {
514
+ if (typeof cancelUploadId !== 'string') {
515
+ return Promise.reject(new Error('Upload ID must be a string'));
516
+ }
517
+ return NativeModule.cancelUpload(cancelUploadId);
518
+ };
519
+
520
+ export const addListener = (
521
+ eventType: UploadEvent,
522
+ uploadId: string,
523
+ listener: (data: UploadEventData) => void,
524
+ ): EmitterSubscription => {
525
+ return DeviceEventEmitter.addListener(
526
+ eventPrefix + eventType,
527
+ (data: UploadEventData) => {
528
+ if (!uploadId || !data || !('id' in data) || data.id === uploadId) {
529
+ listener(data);
530
+ }
531
+ },
532
+ );
533
+ };
534
+
535
+ export const canSuspendIfBackground = (): void => {
536
+ if (Platform.OS === 'ios') {
537
+ NativeModule.canSuspendIfBackground();
538
+ }
539
+ };
540
+
541
+ export const shouldLimitNetwork = (limit: boolean): void => {
542
+ NativeModule.shouldLimitNetwork(limit);
543
+ };
544
+
545
+ export const getAllUploads = (): Promise<NativeUploadInfo[]> => {
546
+ return NativeModule.getAllUploads();
547
+ };
548
+
549
+ export { Upload };
550
+
551
+ export default Upload;