@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,196 @@
1
+ import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
2
+ import type { UploadState } from "../hooks/use-upload";
3
+ import { formatFileSize } from "../utils";
4
+
5
+ export interface UploadProgressProps {
6
+ /** Upload state information */
7
+ state: UploadState;
8
+ /** Optional custom label */
9
+ label?: string;
10
+ }
11
+
12
+ /**
13
+ * Component to display upload progress with percentage, size, and speed
14
+ */
15
+ export function UploadProgress({ state, label }: UploadProgressProps) {
16
+ const getStatusColor = () => {
17
+ switch (state.status) {
18
+ case "uploading":
19
+ return "#007AFF";
20
+ case "success":
21
+ return "#34C759";
22
+ case "error":
23
+ case "aborted":
24
+ return "#FF3B30";
25
+ default:
26
+ return "#999999";
27
+ }
28
+ };
29
+
30
+ const renderContent = () => {
31
+ switch (state.status) {
32
+ case "idle":
33
+ return (
34
+ <View style={styles.container}>
35
+ <Text style={styles.label}>{label || "Ready to upload"}</Text>
36
+ </View>
37
+ );
38
+
39
+ case "uploading":
40
+ return (
41
+ <View style={styles.container}>
42
+ <View style={styles.headerRow}>
43
+ <Text style={styles.label}>{label || "Uploading"}</Text>
44
+ <Text style={styles.percentage}>{state.progress}%</Text>
45
+ </View>
46
+
47
+ {/* Progress bar */}
48
+ <View style={styles.progressBarContainer}>
49
+ <View
50
+ style={[
51
+ styles.progressBar,
52
+ {
53
+ width: `${state.progress}%`,
54
+ backgroundColor: getStatusColor(),
55
+ },
56
+ ]}
57
+ />
58
+ </View>
59
+
60
+ {/* Details row */}
61
+ <View style={styles.detailsRow}>
62
+ <Text style={styles.detail}>
63
+ {formatFileSize(state.bytesUploaded)} /{" "}
64
+ {formatFileSize(state.totalBytes || 0)}
65
+ </Text>
66
+ </View>
67
+ </View>
68
+ );
69
+
70
+ case "success":
71
+ return (
72
+ <View style={styles.container}>
73
+ <View style={styles.headerRow}>
74
+ <Text style={[styles.label, { color: getStatusColor() }]}>
75
+ {label || "Upload complete"}
76
+ </Text>
77
+ <Text style={[styles.percentage, { color: getStatusColor() }]}>
78
+
79
+ </Text>
80
+ </View>
81
+ <Text style={[styles.detail, { color: getStatusColor() }]}>
82
+ {formatFileSize(state.totalBytes || 0)}
83
+ </Text>
84
+ </View>
85
+ );
86
+
87
+ case "error":
88
+ return (
89
+ <View style={styles.container}>
90
+ <View style={styles.headerRow}>
91
+ <Text style={[styles.label, { color: getStatusColor() }]}>
92
+ {label || "Upload failed"}
93
+ </Text>
94
+ <Text style={[styles.percentage, { color: getStatusColor() }]}>
95
+
96
+ </Text>
97
+ </View>
98
+ {state.error && (
99
+ <Text style={[styles.detail, { color: getStatusColor() }]}>
100
+ {state.error.message}
101
+ </Text>
102
+ )}
103
+ </View>
104
+ );
105
+
106
+ case "aborted":
107
+ return (
108
+ <View style={styles.container}>
109
+ <Text style={[styles.label, { color: getStatusColor() }]}>
110
+ {label || "Upload cancelled"}
111
+ </Text>
112
+ </View>
113
+ );
114
+
115
+ default:
116
+ return null;
117
+ }
118
+ };
119
+
120
+ return (
121
+ <View
122
+ style={[
123
+ styles.wrapper,
124
+ {
125
+ borderLeftColor: getStatusColor(),
126
+ },
127
+ ]}
128
+ >
129
+ {state.status === "uploading" && (
130
+ <ActivityIndicator
131
+ size="small"
132
+ color={getStatusColor()}
133
+ style={styles.spinner}
134
+ />
135
+ )}
136
+ {renderContent()}
137
+ </View>
138
+ );
139
+ }
140
+
141
+ const styles = StyleSheet.create({
142
+ wrapper: {
143
+ flexDirection: "row",
144
+ alignItems: "flex-start",
145
+ paddingVertical: 8,
146
+ paddingHorizontal: 12,
147
+ borderLeftWidth: 4,
148
+ backgroundColor: "#f5f5f5",
149
+ borderRadius: 4,
150
+ gap: 8,
151
+ },
152
+ spinner: {
153
+ marginTop: 4,
154
+ },
155
+ container: {
156
+ flex: 1,
157
+ gap: 4,
158
+ },
159
+ headerRow: {
160
+ flexDirection: "row",
161
+ justifyContent: "space-between",
162
+ alignItems: "center",
163
+ },
164
+ label: {
165
+ fontSize: 14,
166
+ fontWeight: "600",
167
+ color: "#333333",
168
+ flex: 1,
169
+ },
170
+ percentage: {
171
+ fontSize: 14,
172
+ fontWeight: "600",
173
+ color: "#007AFF",
174
+ minWidth: 36,
175
+ textAlign: "right",
176
+ },
177
+ progressBarContainer: {
178
+ height: 4,
179
+ backgroundColor: "#e0e0e0",
180
+ borderRadius: 2,
181
+ overflow: "hidden",
182
+ },
183
+ progressBar: {
184
+ height: "100%",
185
+ borderRadius: 2,
186
+ },
187
+ detailsRow: {
188
+ flexDirection: "row",
189
+ justifyContent: "space-between",
190
+ alignItems: "center",
191
+ },
192
+ detail: {
193
+ fontSize: 12,
194
+ color: "#666666",
195
+ },
196
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * React Native UI Components for Uploadista
3
+ * Provides unstyled, customizable components for upload workflows
4
+ */
5
+
6
+ export {
7
+ CameraUploadButton,
8
+ type CameraUploadButtonProps,
9
+ } from "./CameraUploadButton";
10
+ export {
11
+ FileUploadButton,
12
+ type FileUploadButtonProps,
13
+ } from "./FileUploadButton";
14
+ export {
15
+ GalleryUploadButton,
16
+ type GalleryUploadButtonProps,
17
+ } from "./GalleryUploadButton";
18
+ export { UploadList, type UploadListProps } from "./UploadList";
19
+ export { UploadProgress, type UploadProgressProps } from "./UploadProgress";
@@ -0,0 +1,29 @@
1
+ // Core context and client
2
+ export {
3
+ UploadistaContext,
4
+ type UploadistaContextType,
5
+ } from "./uploadista-context";
6
+ export { useCameraUpload } from "./use-camera-upload";
7
+ export { useFileUpload } from "./use-file-upload";
8
+ export { useFlowUpload } from "./use-flow-upload";
9
+ export { useGalleryUpload } from "./use-gallery-upload";
10
+ // Multi-upload hooks
11
+ export type {
12
+ MultiUploadState,
13
+ UploadItemState,
14
+ } from "./use-multi-upload";
15
+ export { useMultiUpload } from "./use-multi-upload";
16
+ // Upload hooks
17
+ export type {
18
+ UploadState,
19
+ UploadStatus,
20
+ UseUploadOptions,
21
+ UseUploadReturn,
22
+ } from "./use-upload";
23
+ export { useUpload } from "./use-upload";
24
+ export { useUploadMetrics } from "./use-upload-metrics";
25
+ export type {
26
+ UseUploadistaClientOptions,
27
+ UseUploadistaClientReturn,
28
+ } from "./use-uploadista-client";
29
+ export { useUploadistaContext } from "./use-uploadista-context";
@@ -0,0 +1,17 @@
1
+ import type { UploadistaEvent } from "@uploadista/client-core";
2
+ import { createContext } from "react";
3
+ import type { FileSystemProvider } from "../types";
4
+ import type { UseUploadistaClientReturn } from "./use-uploadista-client";
5
+
6
+ export interface UploadistaContextType extends UseUploadistaClientReturn {
7
+ fileSystemProvider: FileSystemProvider;
8
+ /**
9
+ * Subscribe to events (used internally by hooks)
10
+ * @internal
11
+ */
12
+ subscribeToEvents: (handler: (event: UploadistaEvent) => void) => () => void;
13
+ }
14
+
15
+ export const UploadistaContext = createContext<
16
+ UploadistaContextType | undefined
17
+ >(undefined);
@@ -0,0 +1,38 @@
1
+ import { useCallback } from "react";
2
+ import type { UseCameraUploadOptions } from "../types";
3
+ import { useUpload } from "./use-upload";
4
+ import { useUploadistaContext } from "./use-uploadista-context";
5
+
6
+ /**
7
+ * Hook for capturing photos and uploading them
8
+ * Handles camera permissions and capture flow
9
+ * @param options - Camera upload configuration
10
+ * @returns Upload state and camera capture/upload function
11
+ */
12
+ export function useCameraUpload(options?: UseCameraUploadOptions) {
13
+ const { fileSystemProvider } = useUploadistaContext();
14
+ const uploadHook = useUpload({
15
+ metadata: options?.metadata,
16
+ onSuccess: options?.onSuccess,
17
+ onError: options?.onError,
18
+ onProgress: options?.onProgress,
19
+ });
20
+
21
+ // Capture and upload photo
22
+ const captureAndUpload = useCallback(async () => {
23
+ try {
24
+ // Capture photo with camera
25
+ const photo = await fileSystemProvider.pickCamera(options?.cameraOptions);
26
+
27
+ // Upload captured photo
28
+ await uploadHook.upload(photo);
29
+ } catch (error) {
30
+ console.error("Camera capture error:", error);
31
+ }
32
+ }, [fileSystemProvider, options?.cameraOptions, uploadHook]);
33
+
34
+ return {
35
+ ...uploadHook,
36
+ captureAndUpload,
37
+ };
38
+ }
@@ -0,0 +1,40 @@
1
+ import { useCallback } from "react";
2
+ import type { UseFileUploadOptions } from "../types";
3
+ import { useUpload } from "./use-upload";
4
+ import { useUploadistaContext } from "./use-uploadista-context";
5
+
6
+ /**
7
+ * Hook for selecting and uploading generic files (documents, etc.)
8
+ * @param options - File upload configuration
9
+ * @returns Upload state and file picker/upload function
10
+ */
11
+ export function useFileUpload(options?: UseFileUploadOptions) {
12
+ const { fileSystemProvider } = useUploadistaContext();
13
+ const uploadHook = useUpload({
14
+ metadata: options?.metadata,
15
+ onSuccess: options?.onSuccess,
16
+ onError: options?.onError,
17
+ onProgress: options?.onProgress,
18
+ });
19
+
20
+ // Pick and upload file
21
+ const pickAndUpload = useCallback(async () => {
22
+ try {
23
+ // Pick file
24
+ const file = await fileSystemProvider.pickDocument({
25
+ allowedTypes: options?.allowedTypes,
26
+ });
27
+
28
+ // Upload file
29
+ await uploadHook.upload(file);
30
+ } catch (error) {
31
+ console.error("File selection error:", error);
32
+ throw error;
33
+ }
34
+ }, [fileSystemProvider, options?.allowedTypes, uploadHook]);
35
+
36
+ return {
37
+ ...uploadHook,
38
+ pickAndUpload,
39
+ };
40
+ }
@@ -0,0 +1,242 @@
1
+ import type { UploadFile } from "@uploadista/core/types";
2
+ import { useCallback, useRef, useState } from "react";
3
+ import type { FilePickResult, UseFlowUploadOptions } from "../types";
4
+ import { useUploadistaContext } from "./use-uploadista-context";
5
+
6
+ export type FlowUploadStatus =
7
+ | "idle"
8
+ | "uploading"
9
+ | "processing"
10
+ | "success"
11
+ | "error"
12
+ | "aborted";
13
+
14
+ export interface FlowUploadState {
15
+ status: FlowUploadStatus;
16
+ progress: number;
17
+ bytesUploaded: number;
18
+ totalBytes: number | null;
19
+ jobId: string | null;
20
+ error: Error | null;
21
+ result: unknown | null;
22
+ }
23
+
24
+ const initialState: FlowUploadState = {
25
+ status: "idle",
26
+ progress: 0,
27
+ bytesUploaded: 0,
28
+ totalBytes: null,
29
+ jobId: null,
30
+ error: null,
31
+ result: null,
32
+ };
33
+
34
+ /**
35
+ * Hook for uploading files through a flow pipeline with full state management.
36
+ * Provides upload progress tracking, flow execution monitoring, error handling, and abort functionality.
37
+ *
38
+ * Must be used within an UploadistaProvider.
39
+ *
40
+ * @param options - Flow upload configuration
41
+ * @returns Flow upload state and control methods
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * function MyComponent() {
46
+ * const flowUpload = useFlowUpload({
47
+ * flowId: 'image-processing-flow',
48
+ * storageId: 'my-storage',
49
+ * onSuccess: (result) => console.log('Flow complete:', result),
50
+ * onError: (error) => console.error('Flow failed:', error),
51
+ * onProgress: (progress) => console.log('Progress:', progress + '%'),
52
+ * });
53
+ *
54
+ * const handlePickFile = async () => {
55
+ * const file = await fileSystemProvider.pickDocument();
56
+ * if (file) {
57
+ * await flowUpload.upload(file);
58
+ * }
59
+ * };
60
+ *
61
+ * return (
62
+ * <View>
63
+ * <Button title="Pick File" onPress={handlePickFile} />
64
+ * {flowUpload.isUploading && <Text>Progress: {flowUpload.state.progress}%</Text>}
65
+ * {flowUpload.state.jobId && <Text>Job ID: {flowUpload.state.jobId}</Text>}
66
+ * {flowUpload.state.error && <Text>Error: {flowUpload.state.error.message}</Text>}
67
+ * <Button title="Abort" onPress={flowUpload.abort} disabled={!flowUpload.isActive} />
68
+ * </View>
69
+ * );
70
+ * }
71
+ * ```
72
+ */
73
+ export function useFlowUpload(options: UseFlowUploadOptions) {
74
+ const { client, fileSystemProvider } = useUploadistaContext();
75
+ const [state, setState] = useState<FlowUploadState>(initialState);
76
+ const abortControllerRef = useRef<{ abort: () => void } | null>(null);
77
+ const lastFileRef = useRef<FilePickResult | null>(null);
78
+
79
+ const updateState = useCallback((update: Partial<FlowUploadState>) => {
80
+ setState((prev) => ({ ...prev, ...update }));
81
+ }, []);
82
+
83
+ const reset = useCallback(() => {
84
+ if (abortControllerRef.current) {
85
+ abortControllerRef.current.abort();
86
+ abortControllerRef.current = null;
87
+ }
88
+ setState(initialState);
89
+ lastFileRef.current = null;
90
+ }, []);
91
+
92
+ const abort = useCallback(() => {
93
+ if (abortControllerRef.current) {
94
+ abortControllerRef.current.abort();
95
+ abortControllerRef.current = null;
96
+ }
97
+
98
+ updateState({
99
+ status: "aborted",
100
+ });
101
+ }, [updateState]);
102
+
103
+ const upload = useCallback(
104
+ async (file: FilePickResult) => {
105
+ // Reset any previous state
106
+ setState({
107
+ ...initialState,
108
+ status: "uploading",
109
+ totalBytes: file.size,
110
+ });
111
+
112
+ lastFileRef.current = file;
113
+
114
+ try {
115
+ // Read file content
116
+ const fileContent = await fileSystemProvider.readFile(file.uri);
117
+
118
+ // Create a Blob from the file content
119
+ // Convert ArrayBuffer to Uint8Array for better compatibility
120
+ const data =
121
+ fileContent instanceof ArrayBuffer
122
+ ? new Uint8Array(fileContent)
123
+ : fileContent;
124
+ // Note: Using any cast here because React Native Blob accepts BufferSource
125
+ // but TypeScript's lib.dom.d.ts Blob type doesn't include it
126
+ // biome-ignore lint/suspicious/noExplicitAny: React Native Blob accepts BufferSource
127
+ const blob = new Blob([data as any], {
128
+ type: file.mimeType || "application/octet-stream",
129
+ // biome-ignore lint/suspicious/noExplicitAny: BlobPropertyBag type differs by platform
130
+ } as any);
131
+
132
+ // use the Blob (for React Native)
133
+ const uploadInput = blob;
134
+
135
+ // Start the flow upload using the client
136
+ const uploadPromise = client.uploadWithFlow(
137
+ uploadInput,
138
+ {
139
+ flowId: options.flowId,
140
+ storageId: options.storageId,
141
+ outputNodeId: options.outputNodeId,
142
+ metadata: options.metadata as Record<string, string> | undefined,
143
+ },
144
+ {
145
+ onJobStart: () => {
146
+ updateState({
147
+ status: "processing",
148
+ });
149
+ },
150
+
151
+ onProgress: (
152
+ _uploadId: string,
153
+ bytesUploaded: number,
154
+ totalBytes: number | null,
155
+ ) => {
156
+ const progress = totalBytes
157
+ ? Math.round((bytesUploaded / totalBytes) * 100)
158
+ : 0;
159
+
160
+ updateState({
161
+ progress,
162
+ bytesUploaded,
163
+ totalBytes,
164
+ });
165
+
166
+ options.onProgress?.(progress, bytesUploaded, totalBytes);
167
+ },
168
+
169
+ onChunkComplete: (
170
+ chunkSize: number,
171
+ bytesAccepted: number,
172
+ bytesTotal: number | null,
173
+ ) => {
174
+ options.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
175
+ },
176
+
177
+ onSuccess: (result: UploadFile) => {
178
+ updateState({
179
+ status: "success",
180
+ result,
181
+ progress: 100,
182
+ bytesUploaded: result.size || 0,
183
+ totalBytes: result.size || null,
184
+ });
185
+
186
+ options.onSuccess?.(result);
187
+ abortControllerRef.current = null;
188
+ },
189
+
190
+ onError: (error: Error) => {
191
+ updateState({
192
+ status: "error",
193
+ error,
194
+ });
195
+
196
+ options.onError?.(error);
197
+ abortControllerRef.current = null;
198
+ },
199
+ },
200
+ );
201
+
202
+ // Handle the promise to get the abort controller
203
+ const controller = await uploadPromise;
204
+ abortControllerRef.current = controller;
205
+ } catch (error) {
206
+ updateState({
207
+ status: "error",
208
+ error: error as Error,
209
+ });
210
+
211
+ options.onError?.(error as Error);
212
+ abortControllerRef.current = null;
213
+ }
214
+ },
215
+ [client, fileSystemProvider, options, updateState],
216
+ );
217
+
218
+ const retry = useCallback(() => {
219
+ if (
220
+ lastFileRef.current &&
221
+ (state.status === "error" || state.status === "aborted")
222
+ ) {
223
+ upload(lastFileRef.current);
224
+ }
225
+ }, [upload, state.status]);
226
+
227
+ const isActive =
228
+ state.status === "uploading" || state.status === "processing";
229
+ const canRetry =
230
+ (state.status === "error" || state.status === "aborted") &&
231
+ lastFileRef.current !== null;
232
+
233
+ return {
234
+ state,
235
+ upload,
236
+ abort,
237
+ reset,
238
+ retry,
239
+ isActive,
240
+ canRetry,
241
+ };
242
+ }
@@ -0,0 +1,65 @@
1
+ import { useCallback } from "react";
2
+ import type { FilePickResult, UseGalleryUploadOptions } from "../types";
3
+ import { useMultiUpload } from "./use-multi-upload";
4
+ import { useUploadistaContext } from "./use-uploadista-context";
5
+
6
+ /**
7
+ * Hook for selecting and uploading photos/videos from gallery
8
+ * Handles batch selection and concurrent uploads
9
+ * @param options - Gallery upload configuration
10
+ * @returns Upload state and gallery selection/upload function
11
+ */
12
+ export function useGalleryUpload(options?: UseGalleryUploadOptions) {
13
+ const { fileSystemProvider } = useUploadistaContext();
14
+ const uploadHook = useMultiUpload({
15
+ maxConcurrent: 3,
16
+ metadata: options?.metadata,
17
+ onSuccess: options?.onSuccess,
18
+ onError: options?.onError,
19
+ });
20
+
21
+ // Select and upload media from gallery
22
+ const selectAndUpload = useCallback(async () => {
23
+ try {
24
+ let media: FilePickResult | FilePickResult[];
25
+
26
+ // Select appropriate media type
27
+ if (options?.mediaType === "video") {
28
+ media = await fileSystemProvider.pickVideo({
29
+ allowMultiple: options?.allowMultiple ?? true,
30
+ });
31
+ } else if (options?.mediaType === "photo") {
32
+ media = await fileSystemProvider.pickImage({
33
+ allowMultiple: options?.allowMultiple ?? true,
34
+ });
35
+ } else {
36
+ // For 'mixed' or default, use pickImage first (can be extended to support both)
37
+ media = await fileSystemProvider.pickImage({
38
+ allowMultiple: options?.allowMultiple ?? true,
39
+ });
40
+ }
41
+
42
+ // Handle single or multiple files
43
+ const files = Array.isArray(media) ? media : [media];
44
+
45
+ // Add files and start upload
46
+ const itemIds = uploadHook.addFiles(files);
47
+ await uploadHook.startUploads();
48
+
49
+ return itemIds;
50
+ } catch (error) {
51
+ console.error("Gallery selection error:", error);
52
+ throw error;
53
+ }
54
+ }, [
55
+ fileSystemProvider,
56
+ options?.allowMultiple,
57
+ options?.mediaType,
58
+ uploadHook,
59
+ ]);
60
+
61
+ return {
62
+ ...uploadHook,
63
+ selectAndUpload,
64
+ };
65
+ }