@uploadista/expo 0.0.8 → 0.0.10

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,188 @@
1
+ "use client";
2
+ import type { UploadistaEvent } from "@uploadista/client-core";
3
+ import { UploadistaContext } from "@uploadista/react-native-core";
4
+ import type { UploadistaContextType } from "@uploadista/react-native-core/hooks";
5
+ import type React from "react";
6
+ import { useCallback, useContext, useMemo, useRef } from "react";
7
+ import {
8
+ type UseUploadistaClientOptions,
9
+ useUploadistaClient,
10
+ } from "../hooks/use-uploadista-client";
11
+ import { ExpoFileSystemProvider } from "../services/expo-file-system-provider";
12
+
13
+ /**
14
+ * Props for the UploadistaProvider component.
15
+ * Combines client configuration options with React children.
16
+ *
17
+ * @property children - React components that will have access to the upload client context
18
+ * @property baseUrl - API base URL for uploads
19
+ * @property storageId - Default storage identifier
20
+ * @property chunkSize - Upload chunk size in bytes
21
+ * @property onEvent - Global event handler for all upload events
22
+ * @property ... - All other UploadistaClientOptions
23
+ */
24
+ export interface UploadistaProviderProps extends UseUploadistaClientOptions {
25
+ /**
26
+ * Children components that will have access to the upload client
27
+ */
28
+ children: React.ReactNode;
29
+ }
30
+
31
+ /**
32
+ * Context provider that provides uploadista client functionality to child components.
33
+ * This eliminates the need to pass upload client configuration down through props
34
+ * and ensures a single, shared upload client instance across your application.
35
+ *
36
+ * @param props - Upload client options and children
37
+ * @returns Provider component with upload client context
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * // Wrap your app with the upload provider
42
+ * function App() {
43
+ * return (
44
+ * <UploadistaProvider
45
+ * baseUrl="https://api.example.com"
46
+ * storageId="my-storage"
47
+ * chunkSize={1024 * 1024} // 1MB chunks
48
+ * onEvent={(event) => {
49
+ * console.log('Global upload event:', event);
50
+ * }}
51
+ * >
52
+ * <UploadInterface />
53
+ * </UploadistaProvider>
54
+ * );
55
+ * }
56
+ *
57
+ * // Use the upload client in any child component
58
+ * function UploadInterface() {
59
+ * const uploadClient = useUploadistaContext();
60
+ * const upload = useUpload(uploadClient);
61
+ * const dragDrop = useDragDrop({
62
+ * onFilesReceived: (files) => {
63
+ * files.forEach(file => upload.upload(file));
64
+ * }
65
+ * });
66
+ *
67
+ * return (
68
+ * <div {...dragDrop.dragHandlers}>
69
+ * <p>Drop files here to upload</p>
70
+ * {upload.isUploading && <p>Progress: {upload.state.progress}%</p>}
71
+ * </div>
72
+ * );
73
+ * }
74
+ * ```
75
+ */
76
+ export function UploadistaProvider({
77
+ children,
78
+ ...options
79
+ }: UploadistaProviderProps) {
80
+ const eventSubscribersRef = useRef<Set<(event: UploadistaEvent) => void>>(
81
+ new Set(),
82
+ );
83
+
84
+ // Create file system provider instance (memoized to avoid recreation)
85
+ const fileSystemProvider = useMemo(() => new ExpoFileSystemProvider(), []);
86
+
87
+ // Wrap the original onEvent to broadcast to subscribers
88
+ const wrappedOnEvent = useCallback(
89
+ (event: UploadistaEvent) => {
90
+ console.log("[UploadistaProvider] Received event:", event);
91
+
92
+ // Call original handler if provided
93
+ options.onEvent?.(event);
94
+
95
+ // Broadcast to all subscribers
96
+ console.log(
97
+ "[UploadistaProvider] Broadcasting to",
98
+ eventSubscribersRef.current.size,
99
+ "subscribers",
100
+ );
101
+ eventSubscribersRef.current.forEach((handler) => {
102
+ try {
103
+ handler(event);
104
+ } catch (err) {
105
+ console.error("Error in event subscriber:", err);
106
+ }
107
+ });
108
+ },
109
+ [options.onEvent],
110
+ );
111
+
112
+ const uploadClient = useUploadistaClient({
113
+ ...options,
114
+ onEvent: wrappedOnEvent,
115
+ });
116
+
117
+ const subscribeToEvents = useCallback(
118
+ (handler: (event: UploadistaEvent) => void) => {
119
+ eventSubscribersRef.current.add(handler);
120
+ return () => {
121
+ eventSubscribersRef.current.delete(handler);
122
+ };
123
+ },
124
+ [],
125
+ );
126
+
127
+ // Memoize the context value to prevent unnecessary re-renders
128
+ const contextValue: UploadistaContextType = useMemo(
129
+ () => ({
130
+ ...uploadClient,
131
+ fileSystemProvider,
132
+ subscribeToEvents,
133
+ // Cast config to match react-native-core expectations (Expo options are compatible)
134
+ // biome-ignore lint/suspicious/noExplicitAny: Type compatibility between Expo and RN Core client options
135
+ config: uploadClient.config as any,
136
+ }),
137
+ [uploadClient, fileSystemProvider, subscribeToEvents],
138
+ );
139
+
140
+ return (
141
+ <UploadistaContext.Provider value={contextValue}>
142
+ {children}
143
+ </UploadistaContext.Provider>
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Hook to access the uploadista client from the UploadistaProvider context.
149
+ * Must be used within an UploadistaProvider component.
150
+ *
151
+ * @returns Upload client instance from context including file system provider
152
+ * @throws Error if used outside of UploadistaProvider
153
+ *
154
+ * @example
155
+ * ```tsx
156
+ * function FileUploader() {
157
+ * const uploadContext = useUploadistaContext();
158
+ * const { client, fileSystemProvider } = uploadContext;
159
+ *
160
+ * const handleFilePick = async () => {
161
+ * try {
162
+ * const result = await fileSystemProvider.pickDocument();
163
+ * await client.upload(result.uri);
164
+ * } catch (error) {
165
+ * console.error('Upload failed:', error);
166
+ * }
167
+ * };
168
+ *
169
+ * return (
170
+ * <button onClick={handleFilePick}>
171
+ * Upload File
172
+ * </button>
173
+ * );
174
+ * }
175
+ * ```
176
+ */
177
+ export function useUploadistaContext(): UploadistaContextType {
178
+ const context = useContext(UploadistaContext);
179
+
180
+ if (context === undefined) {
181
+ throw new Error(
182
+ "useUploadistaContext must be used within an UploadistaProvider. " +
183
+ "Make sure to wrap your component tree with <UploadistaProvider>.",
184
+ );
185
+ }
186
+
187
+ return context;
188
+ }
@@ -0,0 +1,147 @@
1
+ import { useMemo, useRef } from "react";
2
+ import {
3
+ createUploadistaClient,
4
+ type UploadistaClientOptions,
5
+ } from "../client/create-uploadista-client";
6
+
7
+ /**
8
+ * Configuration options for the uploadista client hook.
9
+ * Extends the base client options with React-specific behavior.
10
+ *
11
+ * @property onEvent - Global event handler for all upload and flow events
12
+ * @property baseUrl - API base URL for uploads
13
+ * @property storageId - Default storage identifier
14
+ * @property chunkSize - Size of upload chunks in bytes
15
+ * @property storeFingerprintForResuming - Enable resumable uploads
16
+ * @property retryDelays - Array of retry delays in milliseconds
17
+ * @property parallelUploads - Maximum number of parallel uploads
18
+ * @property uploadStrategy - Upload strategy (sequential, parallel, adaptive)
19
+ * @property smartChunking - Enable dynamic chunk size adjustment
20
+ * @property networkMonitoring - Enable network condition monitoring
21
+ * @property useAsyncStorage - Whether to use AsyncStorage for persistence (default: true)
22
+ */
23
+ export interface UseUploadistaClientOptions extends UploadistaClientOptions {
24
+ /**
25
+ * Global event handler for all upload and flow events from this client
26
+ */
27
+ onEvent?: UploadistaClientOptions["onEvent"];
28
+ }
29
+
30
+ /**
31
+ * Return value from the useUploadistaClient hook.
32
+ *
33
+ * @property client - Configured uploadista client instance (stable across re-renders)
34
+ * @property config - Current client configuration options
35
+ */
36
+ export interface UseUploadistaClientReturn {
37
+ /**
38
+ * The uploadista client instance
39
+ */
40
+ client: ReturnType<typeof createUploadistaClient>;
41
+
42
+ /**
43
+ * Current configuration of the client
44
+ */
45
+ config: UseUploadistaClientOptions;
46
+ }
47
+
48
+ /**
49
+ * React hook for creating and managing an uploadista client instance for Expo.
50
+ * The client instance is memoized and stable across re-renders, only being
51
+ * recreated when configuration options change.
52
+ *
53
+ * This hook is typically used internally by UploadistaProvider, but can be
54
+ * used directly for advanced use cases requiring multiple client instances.
55
+ *
56
+ * @param options - Upload client configuration options
57
+ * @returns Object containing the stable client instance and current configuration
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * // Basic client setup
62
+ * function MyUploadComponent() {
63
+ * const { client, config } = useUploadistaClient({
64
+ * baseUrl: 'https://api.example.com',
65
+ * storageId: 'default-storage',
66
+ * chunkSize: 1024 * 1024, // 1MB chunks
67
+ * storeFingerprintForResuming: true,
68
+ * useAsyncStorage: true, // Use AsyncStorage for persistence
69
+ * onEvent: (event) => {
70
+ * console.log('Upload event:', event);
71
+ * }
72
+ * });
73
+ *
74
+ * // Use client directly
75
+ * const handleUpload = async (uri: string) => {
76
+ * await client.upload(uri, {
77
+ * onSuccess: (result) => console.log('Uploaded:', result),
78
+ * onError: (error) => console.error('Failed:', error),
79
+ * });
80
+ * };
81
+ *
82
+ * return <FileUploader onUpload={handleUpload} />;
83
+ * }
84
+ *
85
+ * // Advanced: Multiple clients with different configurations
86
+ * function MultiClientComponent() {
87
+ * // Client for image uploads
88
+ * const imageClient = useUploadistaClient({
89
+ * baseUrl: 'https://images.example.com',
90
+ * storageId: 'images',
91
+ * chunkSize: 2 * 1024 * 1024, // 2MB for images
92
+ * });
93
+ *
94
+ * // Client for document uploads
95
+ * const docClient = useUploadistaClient({
96
+ * baseUrl: 'https://docs.example.com',
97
+ * storageId: 'documents',
98
+ * chunkSize: 512 * 1024, // 512KB for documents
99
+ * });
100
+ *
101
+ * return (
102
+ * <View>
103
+ * <ImageUploader client={imageClient.client} />
104
+ * <DocumentUploader client={docClient.client} />
105
+ * </View>
106
+ * );
107
+ * }
108
+ * ```
109
+ *
110
+ * @see {@link UploadistaProvider} for the recommended way to provide client context
111
+ */
112
+ export function useUploadistaClient(
113
+ options: UseUploadistaClientOptions,
114
+ ): UseUploadistaClientReturn {
115
+ // Store the options in a ref to enable stable dependency checking
116
+ const optionsRef = useRef<UseUploadistaClientOptions>(options);
117
+
118
+ // Update ref on each render but only create new client when essential deps change
119
+ optionsRef.current = options;
120
+
121
+ // Create client instance with stable identity
122
+ const client = useMemo(() => {
123
+ return createUploadistaClient({
124
+ baseUrl: options.baseUrl,
125
+ storageId: options.storageId,
126
+ uploadistaBasePath: options.uploadistaBasePath,
127
+ chunkSize: options.chunkSize,
128
+ storeFingerprintForResuming: options.storeFingerprintForResuming,
129
+ retryDelays: options.retryDelays,
130
+ parallelUploads: options.parallelUploads,
131
+ parallelChunkSize: options.parallelChunkSize,
132
+ uploadStrategy: options.uploadStrategy,
133
+ smartChunking: options.smartChunking,
134
+ networkMonitoring: options.networkMonitoring,
135
+ uploadMetrics: options.uploadMetrics,
136
+ connectionPooling: options.connectionPooling,
137
+ useAsyncStorage: options.useAsyncStorage,
138
+ auth: options.auth,
139
+ onEvent: options.onEvent,
140
+ });
141
+ }, [options]);
142
+
143
+ return {
144
+ client,
145
+ config: options,
146
+ };
147
+ }
package/src/index.ts CHANGED
@@ -50,11 +50,30 @@ export type {
50
50
  SliceResult,
51
51
  StorageService,
52
52
  } from "@uploadista/client-core";
53
+ // Export Expo-specific types
54
+ export type {
55
+ CameraOptions,
56
+ FileInfo,
57
+ FilePickResult,
58
+ FileSystemProvider,
59
+ PickerOptions,
60
+ } from "@uploadista/react-native-core";
53
61
  // Export client factory
54
62
  export {
55
63
  createUploadistaClient,
56
64
  type UploadistaClientOptions,
57
65
  } from "./client";
66
+ // Export provider and hooks
67
+ export {
68
+ UploadistaProvider,
69
+ type UploadistaProviderProps,
70
+ useUploadistaContext,
71
+ } from "./components/uploadista-provider";
72
+ export {
73
+ type UseUploadistaClientOptions,
74
+ type UseUploadistaClientReturn,
75
+ useUploadistaClient,
76
+ } from "./hooks/use-uploadista-client";
58
77
  // Re-export service implementations and factories
59
78
  export {
60
79
  createAsyncStorageService,
@@ -65,13 +84,4 @@ export {
65
84
  createExpoServices,
66
85
  type ExpoServiceOptions,
67
86
  } from "./services";
68
-
69
87
  export { ExpoFileSystemProvider } from "./services/expo-file-system-provider";
70
- // Export Expo-specific types
71
- export type {
72
- CameraOptions,
73
- FileInfo,
74
- FilePickResult,
75
- FileSystemProvider,
76
- PickerOptions,
77
- } from "./types";
@@ -1,13 +1,13 @@
1
- import * as DocumentPicker from "expo-document-picker";
2
- import * as FileSystem from "expo-file-system";
3
- import * as ImagePicker from "expo-image-picker";
4
1
  import type {
5
2
  CameraOptions,
6
3
  FileInfo,
7
4
  FilePickResult,
8
5
  FileSystemProvider,
9
6
  PickerOptions,
10
- } from "../types";
7
+ } from "@uploadista/react-native-core";
8
+ import * as DocumentPicker from "expo-document-picker";
9
+ import * as FileSystem from "expo-file-system";
10
+ import * as ImagePicker from "expo-image-picker";
11
11
 
12
12
  /**
13
13
  * File system provider implementation for Expo managed environment
@@ -30,27 +30,33 @@ export class ExpoFileSystemProvider implements FileSystemProvider {
30
30
  };
31
31
 
32
32
  if (result.canceled) {
33
- throw new Error("Document picker was cancelled");
33
+ return { status: "cancelled" };
34
34
  }
35
35
 
36
36
  const asset = result.assets?.[0];
37
37
  if (!asset) {
38
- throw new Error("No document selected");
38
+ return { status: "cancelled" };
39
39
  }
40
40
 
41
41
  return {
42
- uri: asset.uri,
43
- name: asset.name,
44
- size: asset.size || 0,
45
- mimeType: asset.mimeType,
42
+ status: "success",
43
+ data: {
44
+ uri: asset.uri,
45
+ name: asset.name,
46
+ size: asset.size || 0,
47
+ mimeType: asset.mimeType,
48
+ },
46
49
  };
47
50
  } catch (error) {
48
- if (error instanceof Error && error.message.includes("cancelled")) {
49
- throw error;
50
- }
51
- throw new Error(
52
- `Failed to pick document: ${error instanceof Error ? error.message : String(error)}`,
53
- );
51
+ return {
52
+ status: "error",
53
+ error:
54
+ error instanceof Error
55
+ ? error
56
+ : new Error(
57
+ `Failed to pick document: ${error instanceof Error ? error.message : String(error)}`,
58
+ ),
59
+ };
54
60
  }
55
61
  }
56
62
 
@@ -60,38 +66,46 @@ export class ExpoFileSystemProvider implements FileSystemProvider {
60
66
  const { status } =
61
67
  await ImagePicker.requestMediaLibraryPermissionsAsync();
62
68
  if (status !== "granted") {
63
- throw new Error("Camera roll permission not granted");
69
+ return {
70
+ status: "error",
71
+ error: new Error("Camera roll permission not granted"),
72
+ };
64
73
  }
65
74
 
66
75
  const result = await ImagePicker.launchImageLibraryAsync({
67
- // biome-ignore lint/suspicious/noExplicitAny: Expo ImagePicker mediaTypes type compatibility
68
- mediaTypes: "Images" as any,
76
+ mediaTypes: "images",
69
77
  selectionLimit: options?.allowMultiple ? 0 : 1,
70
78
  quality: 1,
71
79
  });
72
80
 
73
81
  if (result.canceled) {
74
- throw new Error("Image picker was cancelled");
82
+ return { status: "cancelled" };
75
83
  }
76
84
 
77
85
  const asset = result.assets?.[0];
78
86
  if (!asset) {
79
- throw new Error("No image selected");
87
+ return { status: "cancelled" };
80
88
  }
81
89
 
82
90
  return {
83
- uri: asset.uri,
84
- name: asset.fileName || `image-${Date.now()}.jpg`,
85
- size: asset.fileSize || 0,
86
- mimeType: "image/jpeg",
91
+ status: "success",
92
+ data: {
93
+ uri: asset.uri,
94
+ name: asset.fileName || `image-${Date.now()}.jpg`,
95
+ size: asset.fileSize || 0,
96
+ mimeType: "image/jpeg",
97
+ },
87
98
  };
88
99
  } catch (error) {
89
- if (error instanceof Error && error.message.includes("cancelled")) {
90
- throw error;
91
- }
92
- throw new Error(
93
- `Failed to pick image: ${error instanceof Error ? error.message : String(error)}`,
94
- );
100
+ return {
101
+ status: "error",
102
+ error:
103
+ error instanceof Error
104
+ ? error
105
+ : new Error(
106
+ `Failed to pick image: ${error instanceof Error ? error.message : String(error)}`,
107
+ ),
108
+ };
95
109
  }
96
110
  }
97
111
 
@@ -101,37 +115,45 @@ export class ExpoFileSystemProvider implements FileSystemProvider {
101
115
  const { status } =
102
116
  await ImagePicker.requestMediaLibraryPermissionsAsync();
103
117
  if (status !== "granted") {
104
- throw new Error("Camera roll permission not granted");
118
+ return {
119
+ status: "error",
120
+ error: new Error("Camera roll permission not granted"),
121
+ };
105
122
  }
106
123
 
107
124
  const result = await ImagePicker.launchImageLibraryAsync({
108
- // biome-ignore lint/suspicious/noExplicitAny: Expo ImagePicker mediaTypes type compatibility
109
- mediaTypes: "Videos" as any,
125
+ mediaTypes: "videos",
110
126
  selectionLimit: options?.allowMultiple ? 0 : 1,
111
127
  });
112
128
 
113
129
  if (result.canceled) {
114
- throw new Error("Video picker was cancelled");
130
+ return { status: "cancelled" };
115
131
  }
116
132
 
117
133
  const asset = result.assets?.[0];
118
134
  if (!asset) {
119
- throw new Error("No video selected");
135
+ return { status: "cancelled" };
120
136
  }
121
137
 
122
138
  return {
123
- uri: asset.uri,
124
- name: asset.fileName || `video-${Date.now()}.mp4`,
125
- size: asset.fileSize || 0,
126
- mimeType: "video/mp4",
139
+ status: "success",
140
+ data: {
141
+ uri: asset.uri,
142
+ name: asset.fileName || `video-${Date.now()}.mp4`,
143
+ size: asset.fileSize || 0,
144
+ mimeType: "video/mp4",
145
+ },
127
146
  };
128
147
  } catch (error) {
129
- if (error instanceof Error && error.message.includes("cancelled")) {
130
- throw error;
131
- }
132
- throw new Error(
133
- `Failed to pick video: ${error instanceof Error ? error.message : String(error)}`,
134
- );
148
+ return {
149
+ status: "error",
150
+ error:
151
+ error instanceof Error
152
+ ? error
153
+ : new Error(
154
+ `Failed to pick video: ${error instanceof Error ? error.message : String(error)}`,
155
+ ),
156
+ };
135
157
  }
136
158
  }
137
159
 
@@ -140,7 +162,10 @@ export class ExpoFileSystemProvider implements FileSystemProvider {
140
162
  // Request camera permissions
141
163
  const { status } = await ImagePicker.requestCameraPermissionsAsync();
142
164
  if (status !== "granted") {
143
- throw new Error("Camera permission not granted");
165
+ return {
166
+ status: "error",
167
+ error: new Error("Camera permission not granted"),
168
+ };
144
169
  }
145
170
 
146
171
  const result = await ImagePicker.launchCameraAsync({
@@ -150,45 +175,44 @@ export class ExpoFileSystemProvider implements FileSystemProvider {
150
175
  });
151
176
 
152
177
  if (result.canceled) {
153
- throw new Error("Camera capture was cancelled");
178
+ return { status: "cancelled" };
154
179
  }
155
180
 
156
181
  const asset = result.assets?.[0];
157
182
  if (!asset) {
158
- throw new Error("No photo captured");
183
+ return { status: "cancelled" };
159
184
  }
160
185
 
161
186
  return {
162
- uri: asset.uri,
163
- name: asset.fileName || `photo-${Date.now()}.jpg`,
164
- size: asset.fileSize || 0,
165
- mimeType: "image/jpeg",
187
+ status: "success",
188
+ data: {
189
+ uri: asset.uri,
190
+ name: asset.fileName || `photo-${Date.now()}.jpg`,
191
+ size: asset.fileSize || 0,
192
+ mimeType: "image/jpeg",
193
+ },
166
194
  };
167
195
  } catch (error) {
168
- if (error instanceof Error && error.message.includes("cancelled")) {
169
- throw error;
170
- }
171
- throw new Error(
172
- `Failed to capture photo: ${error instanceof Error ? error.message : String(error)}`,
173
- );
196
+ return {
197
+ status: "error",
198
+ error:
199
+ error instanceof Error
200
+ ? error
201
+ : new Error(
202
+ `Failed to capture photo: ${error instanceof Error ? error.message : String(error)}`,
203
+ ),
204
+ };
174
205
  }
175
206
  }
176
207
 
177
208
  async readFile(uri: string): Promise<ArrayBuffer> {
178
209
  try {
179
- // Read file as base64
180
- const base64String = await FileSystem.readAsStringAsync(uri, {
181
- encoding: FileSystem.EncodingType.Base64,
182
- });
183
-
184
- // Convert base64 to ArrayBuffer
185
- // Use js-base64 for decoding since atob is not available in all RN environments
186
- const { fromBase64 } = await import("js-base64");
187
- const binaryString = fromBase64(base64String);
188
- const bytes = new Uint8Array(binaryString.length);
189
- for (let i = 0; i < binaryString.length; i++) {
190
- bytes[i] = binaryString.charCodeAt(i);
210
+ const file = new FileSystem.File(uri);
211
+ if (!file.exists) {
212
+ throw new Error("File does not exist");
191
213
  }
214
+ const bytes = await file.bytes();
215
+
192
216
  return bytes.buffer;
193
217
  } catch (error) {
194
218
  throw new Error(
@@ -204,18 +228,18 @@ export class ExpoFileSystemProvider implements FileSystemProvider {
204
228
 
205
229
  async getFileInfo(uri: string): Promise<FileInfo> {
206
230
  try {
207
- const fileInfo = await FileSystem.getInfoAsync(uri);
231
+ const file = new FileSystem.File(uri);
208
232
 
209
- if (!fileInfo.exists) {
233
+ if (!file.exists) {
210
234
  throw new Error("File does not exist");
211
235
  }
212
236
 
213
237
  return {
214
238
  uri,
215
239
  name: uri.split("/").pop() || "unknown",
216
- size: fileInfo.size ?? 0,
217
- modificationTime: fileInfo.modificationTime
218
- ? fileInfo.modificationTime * 1000
240
+ size: file.size ?? 0,
241
+ modificationTime: file.modificationTime
242
+ ? file.modificationTime * 1000
219
243
  : undefined,
220
244
  };
221
245
  } catch (error) {
@@ -3,8 +3,8 @@ import type {
3
3
  FileSource,
4
4
  SliceResult,
5
5
  } from "@uploadista/client-core";
6
+ // import { Blob } from "expo-blob";
6
7
  import type { ExpoUploadInput } from "@/types/upload-input";
7
-
8
8
  /**
9
9
  * Expo-specific implementation of FileReaderService
10
10
  * Handles Blob, File, and URI-based file inputs using Expo FileSystem APIs