@uploadista/react-native-core 0.0.3

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,378 @@
1
+ import type { UploadistaEvent } from "@uploadista/client-core";
2
+ import type { UploadFile } from "@uploadista/core/types";
3
+ import { UploadEventType } from "@uploadista/core/types";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import type { FilePickResult } from "../types";
6
+ import { useUploadistaContext } from "./use-uploadista-context";
7
+
8
+ export type UploadStatus =
9
+ | "idle"
10
+ | "uploading"
11
+ | "success"
12
+ | "error"
13
+ | "aborted";
14
+
15
+ export interface UploadState {
16
+ status: UploadStatus;
17
+ progress: number;
18
+ bytesUploaded: number;
19
+ totalBytes: number | null;
20
+ error: Error | null;
21
+ result: UploadFile | null;
22
+ }
23
+
24
+ export interface UseUploadOptions {
25
+ /**
26
+ * Upload metadata to attach to the file
27
+ */
28
+ metadata?: Record<string, string>;
29
+
30
+ /**
31
+ * Whether to defer the upload size calculation
32
+ */
33
+ uploadLengthDeferred?: boolean;
34
+
35
+ /**
36
+ * Manual upload size override
37
+ */
38
+ uploadSize?: number;
39
+
40
+ /**
41
+ * Called when upload progress updates
42
+ */
43
+ onProgress?: (
44
+ progress: number,
45
+ bytesUploaded: number,
46
+ totalBytes: number | null,
47
+ ) => void;
48
+
49
+ /**
50
+ * Called when a chunk completes
51
+ */
52
+ onChunkComplete?: (
53
+ chunkSize: number,
54
+ bytesAccepted: number,
55
+ bytesTotal: number | null,
56
+ ) => void;
57
+
58
+ /**
59
+ * Called when upload succeeds
60
+ */
61
+ onSuccess?: (result: UploadFile) => void;
62
+
63
+ /**
64
+ * Called when upload fails
65
+ */
66
+ onError?: (error: Error) => void;
67
+
68
+ /**
69
+ * Called when upload is aborted
70
+ */
71
+ onAbort?: () => void;
72
+
73
+ /**
74
+ * Custom retry logic
75
+ */
76
+ onShouldRetry?: (error: Error, retryAttempt: number) => boolean;
77
+ }
78
+
79
+ export interface UseUploadReturn {
80
+ /**
81
+ * Current upload state
82
+ */
83
+ state: UploadState;
84
+
85
+ /**
86
+ * Start uploading a file from a file pick result
87
+ */
88
+ upload: (file: FilePickResult) => Promise<void>;
89
+
90
+ /**
91
+ * Abort the current upload
92
+ */
93
+ abort: () => void;
94
+
95
+ /**
96
+ * Reset the upload state to idle
97
+ */
98
+ reset: () => void;
99
+
100
+ /**
101
+ * Retry the last failed upload
102
+ */
103
+ retry: () => void;
104
+
105
+ /**
106
+ * Whether an upload is currently active
107
+ */
108
+ isUploading: boolean;
109
+
110
+ /**
111
+ * Whether the upload can be retried
112
+ */
113
+ canRetry: boolean;
114
+ }
115
+
116
+ const initialState: UploadState = {
117
+ status: "idle",
118
+ progress: 0,
119
+ bytesUploaded: 0,
120
+ totalBytes: null,
121
+ error: null,
122
+ result: null,
123
+ };
124
+
125
+ /**
126
+ * React hook for managing individual file uploads with full state management.
127
+ * Provides upload progress tracking, error handling, abort functionality, and retry logic.
128
+ *
129
+ * Must be used within an UploadistaProvider.
130
+ *
131
+ * @param options - Upload configuration and event handlers
132
+ * @returns Upload state and control methods
133
+ *
134
+ * @example
135
+ * ```tsx
136
+ * function MyComponent() {
137
+ * const upload = useUpload({
138
+ * onSuccess: (result) => console.log('Upload complete:', result),
139
+ * onError: (error) => console.error('Upload failed:', error),
140
+ * onProgress: (progress) => console.log('Progress:', progress + '%'),
141
+ * });
142
+ *
143
+ * const handlePickFile = async () => {
144
+ * const file = await fileSystemProvider.pickDocument();
145
+ * if (file) {
146
+ * await upload.upload(file);
147
+ * }
148
+ * };
149
+ *
150
+ * return (
151
+ * <View>
152
+ * <Button title="Pick File" onPress={handlePickFile} />
153
+ * {upload.isUploading && <Text>Progress: {upload.state.progress}%</Text>}
154
+ * {upload.state.error && <Text>Error: {upload.state.error.message}</Text>}
155
+ * {upload.canRetry && <Button title="Retry" onPress={upload.retry} />}
156
+ * <Button title="Abort" onPress={upload.abort} disabled={!upload.isUploading} />
157
+ * </View>
158
+ * );
159
+ * }
160
+ * ```
161
+ */
162
+ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
163
+ const { client, fileSystemProvider, subscribeToEvents } =
164
+ useUploadistaContext();
165
+ const [state, setState] = useState<UploadState>(initialState);
166
+ const abortControllerRef = useRef<{ abort: () => void } | null>(null);
167
+ const lastFileRef = useRef<FilePickResult | null>(null);
168
+ const currentUploadIdRef = useRef<string | null>(null);
169
+
170
+ const updateState = useCallback((update: Partial<UploadState>) => {
171
+ setState((prev) => ({ ...prev, ...update }));
172
+ }, []);
173
+
174
+ const reset = useCallback(() => {
175
+ if (abortControllerRef.current) {
176
+ abortControllerRef.current.abort();
177
+ abortControllerRef.current = null;
178
+ }
179
+ setState(initialState);
180
+ lastFileRef.current = null;
181
+ currentUploadIdRef.current = null;
182
+ }, []);
183
+
184
+ const abort = useCallback(() => {
185
+ if (abortControllerRef.current) {
186
+ abortControllerRef.current.abort();
187
+ abortControllerRef.current = null;
188
+ }
189
+
190
+ updateState({
191
+ status: "aborted",
192
+ });
193
+
194
+ options.onAbort?.();
195
+ }, [options, updateState]);
196
+
197
+ const upload = useCallback(
198
+ async (file: FilePickResult) => {
199
+ // Reset any previous state
200
+ setState({
201
+ ...initialState,
202
+ status: "uploading",
203
+ totalBytes: file.size,
204
+ });
205
+
206
+ lastFileRef.current = file;
207
+
208
+ try {
209
+ // Read file content
210
+ const fileContent = await fileSystemProvider.readFile(file.uri);
211
+
212
+ // Create a Blob from the file content
213
+ // Convert ArrayBuffer to Uint8Array for better compatibility
214
+ const data =
215
+ fileContent instanceof ArrayBuffer
216
+ ? new Uint8Array(fileContent)
217
+ : fileContent;
218
+ // Note: Using any cast here because React Native Blob accepts BufferSource
219
+ // but TypeScript's lib.dom.d.ts Blob type doesn't include it
220
+ // biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
221
+ const blob = new Blob([data as any], {
222
+ type: file.mimeType || "application/octet-stream",
223
+ // biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
224
+ } as any);
225
+
226
+ // use the Blob (for React Native)
227
+ const uploadInput = blob;
228
+
229
+ // Start the upload using the client
230
+ const uploadPromise = client.upload(uploadInput, {
231
+ metadata: options.metadata,
232
+ uploadLengthDeferred: options.uploadLengthDeferred,
233
+ uploadSize: options.uploadSize,
234
+
235
+ onStart: ({ uploadId }) => {
236
+ currentUploadIdRef.current = uploadId;
237
+ },
238
+
239
+ onProgress: (
240
+ _uploadId: string,
241
+ bytesUploaded: number,
242
+ totalBytes: number | null,
243
+ ) => {
244
+ const progress = totalBytes
245
+ ? Math.round((bytesUploaded / totalBytes) * 100)
246
+ : 0;
247
+
248
+ updateState({
249
+ progress,
250
+ bytesUploaded,
251
+ totalBytes,
252
+ });
253
+
254
+ options.onProgress?.(progress, bytesUploaded, totalBytes);
255
+ },
256
+
257
+ onChunkComplete: (
258
+ chunkSize: number,
259
+ bytesAccepted: number,
260
+ bytesTotal: number | null,
261
+ ) => {
262
+ options.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
263
+ },
264
+
265
+ onSuccess: (result: UploadFile) => {
266
+ updateState({
267
+ status: "success",
268
+ result,
269
+ progress: 100,
270
+ bytesUploaded: result.size || 0,
271
+ totalBytes: result.size || null,
272
+ });
273
+
274
+ options.onSuccess?.(result);
275
+ abortControllerRef.current = null;
276
+ },
277
+
278
+ onError: (error: Error) => {
279
+ updateState({
280
+ status: "error",
281
+ error,
282
+ });
283
+
284
+ options.onError?.(error);
285
+ abortControllerRef.current = null;
286
+ },
287
+
288
+ onShouldRetry: options.onShouldRetry,
289
+ });
290
+
291
+ // Handle the promise to get the abort controller
292
+ const controller = await uploadPromise;
293
+ abortControllerRef.current = controller;
294
+ } catch (error) {
295
+ updateState({
296
+ status: "error",
297
+ error: error as Error,
298
+ });
299
+
300
+ options.onError?.(error as Error);
301
+ abortControllerRef.current = null;
302
+ }
303
+ },
304
+ [client, fileSystemProvider, options, updateState],
305
+ );
306
+
307
+ const retry = useCallback(() => {
308
+ if (
309
+ lastFileRef.current &&
310
+ (state.status === "error" || state.status === "aborted")
311
+ ) {
312
+ upload(lastFileRef.current);
313
+ }
314
+ }, [upload, state.status]);
315
+
316
+ // Subscribe to events from context (WebSocket events)
317
+ useEffect(() => {
318
+ const unsubscribe = subscribeToEvents((event: UploadistaEvent) => {
319
+ // Handle upload progress events
320
+ const uploadEvent = event as {
321
+ type: string;
322
+ data?: { id: string; progress: number; total: number };
323
+ };
324
+
325
+ if (
326
+ uploadEvent.type === UploadEventType.UPLOAD_PROGRESS &&
327
+ uploadEvent.data
328
+ ) {
329
+ const {
330
+ id: uploadId,
331
+ progress: bytesUploaded,
332
+ total: totalBytes,
333
+ } = uploadEvent.data;
334
+
335
+ if (uploadId !== currentUploadIdRef.current) {
336
+ return;
337
+ }
338
+
339
+ // Update state for this upload
340
+ const progress = totalBytes
341
+ ? Math.round((bytesUploaded / totalBytes) * 100)
342
+ : 0;
343
+
344
+ setState((prev) => {
345
+ // Only update if we're currently uploading
346
+ if (prev.status === "uploading") {
347
+ return {
348
+ ...prev,
349
+ progress,
350
+ bytesUploaded,
351
+ totalBytes,
352
+ };
353
+ }
354
+ return prev;
355
+ });
356
+
357
+ options.onProgress?.(progress, bytesUploaded, totalBytes);
358
+ }
359
+ });
360
+
361
+ return unsubscribe;
362
+ }, [subscribeToEvents, options]);
363
+
364
+ const isUploading = state.status === "uploading";
365
+ const canRetry =
366
+ (state.status === "error" || state.status === "aborted") &&
367
+ lastFileRef.current !== null;
368
+
369
+ return {
370
+ state,
371
+ upload,
372
+ abort,
373
+ reset,
374
+ retry,
375
+ isUploading,
376
+ canRetry,
377
+ };
378
+ }
@@ -0,0 +1,23 @@
1
+ import type {
2
+ createUploadistaClient,
3
+ UploadistaClientOptions,
4
+ } from "../client";
5
+
6
+ export interface UseUploadistaClientOptions extends UploadistaClientOptions {
7
+ /**
8
+ * Global event handler for all upload and flow events from this client
9
+ */
10
+ onEvent?: UploadistaClientOptions["onEvent"];
11
+ }
12
+
13
+ export interface UseUploadistaClientReturn {
14
+ /**
15
+ * The uploadista client instance
16
+ */
17
+ client: ReturnType<typeof createUploadistaClient>;
18
+
19
+ /**
20
+ * Current configuration of the client
21
+ */
22
+ config: UseUploadistaClientOptions;
23
+ }
@@ -0,0 +1,20 @@
1
+ import { useContext } from "react";
2
+ import { UploadistaContext } from "./uploadista-context";
3
+
4
+ /**
5
+ * Hook to access the Uploadista client instance
6
+ * Must be used within an UploadistaProvider
7
+ * @throws Error if used outside of UploadistaProvider
8
+ * @returns The Uploadista client and file system provider
9
+ */
10
+ export function useUploadistaContext() {
11
+ const context = useContext(UploadistaContext);
12
+
13
+ if (!context) {
14
+ throw new Error(
15
+ "useUploadistaClient must be used within an UploadistaProvider",
16
+ );
17
+ }
18
+
19
+ return context;
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @uploadista/react-native - React Native client for Uploadista
3
+ * Provides mobile-optimized hooks and components for file uploads and flow management
4
+ *
5
+ * Usage:
6
+ * ```ts
7
+ * import { createUploadistaClient } from '@uploadista/react-native'
8
+ *
9
+ * const client = createUploadistaClient({
10
+ * baseUrl: 'https://api.example.com',
11
+ * storageId: 'my-storage',
12
+ * chunkSize: 1024 * 1024, // 1MB
13
+ * })
14
+ * ```
15
+ */
16
+
17
+ // Re-export core types from upload-client-core
18
+ export type {
19
+ Base64Service,
20
+ ConnectionMetrics,
21
+ ConnectionPoolConfig,
22
+ DetailedConnectionMetrics,
23
+ FileReaderService,
24
+ HttpClient,
25
+ IdGenerationService,
26
+ ServiceContainer,
27
+ StorageService,
28
+ } from "@uploadista/client-core";
29
+
30
+ // Export components
31
+ export {
32
+ CameraUploadButton,
33
+ type CameraUploadButtonProps,
34
+ FileUploadButton,
35
+ type FileUploadButtonProps,
36
+ GalleryUploadButton,
37
+ type GalleryUploadButtonProps,
38
+ UploadList,
39
+ type UploadListProps,
40
+ UploadProgress,
41
+ type UploadProgressProps,
42
+ } from "./components";
43
+ // Export hooks
44
+ export {
45
+ UploadistaContext,
46
+ type UploadistaContextType,
47
+ useCameraUpload,
48
+ useFileUpload,
49
+ useFlowUpload,
50
+ useGalleryUpload,
51
+ useMultiUpload,
52
+ useUploadistaContext,
53
+ useUploadMetrics,
54
+ } from "./hooks";
55
+ export type {
56
+ FlowUploadState,
57
+ FlowUploadStatus,
58
+ } from "./hooks/use-flow-upload";
59
+ export type {
60
+ MultiUploadState,
61
+ UploadItemState,
62
+ } from "./hooks/use-multi-upload";
63
+ // Export hook types
64
+ export type { UploadState, UploadStatus } from "./hooks/use-upload";
65
+
66
+ // Export types
67
+ export type {
68
+ CameraOptions,
69
+ FileInfo,
70
+ FilePickResult,
71
+ FileSystemProvider,
72
+ FileSystemProviderConfig,
73
+ PickerOptions,
74
+ ReactNativeUploadInput,
75
+ UploadMetrics,
76
+ UseCameraUploadOptions,
77
+ UseFileUploadOptions,
78
+ UseFlowUploadOptions,
79
+ UseGalleryUploadOptions,
80
+ UseMultiUploadOptions,
81
+ } from "./types";
82
+ // Export utilities
83
+ export {
84
+ formatFileSize,
85
+ getDirectoryFromUri,
86
+ getFileExtension,
87
+ getFileNameFromUri,
88
+ getFileNameWithoutExtension,
89
+ getMimeTypeFromFileName,
90
+ getMimeTypeFromUri,
91
+ getPermissionStatus,
92
+ hasPermissions,
93
+ isContentUri,
94
+ isDocumentFile,
95
+ isFileSizeValid,
96
+ isFileTypeAllowed,
97
+ isFileUri,
98
+ isImageFile,
99
+ isVideoFile,
100
+ normalizeUri,
101
+ openAppSettings,
102
+ PermissionStatus,
103
+ PermissionType,
104
+ pathToUri,
105
+ requestCameraPermission,
106
+ requestPermissions,
107
+ requestPhotoLibraryPermission,
108
+ requestStorageReadPermission,
109
+ requestStorageWritePermission,
110
+ uriToPath,
111
+ } from "./utils";
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./upload-input";