@uploadista/react-native-bare 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.
- package/LICENSE +21 -0
- package/README.md +440 -0
- package/package.json +31 -0
- package/src/index.ts +16 -0
- package/src/services/abort-controller-factory.ts +33 -0
- package/src/services/base64-service.ts +29 -0
- package/src/services/create-react-native-services.ts +76 -0
- package/src/services/file-reader-service.ts +144 -0
- package/src/services/http-client.ts +242 -0
- package/src/services/id-generation-service.ts +14 -0
- package/src/services/index.ts +15 -0
- package/src/services/native-file-system-provider.ts +247 -0
- package/src/services/platform-service.ts +71 -0
- package/src/services/storage-service.ts +62 -0
- package/src/services/websocket-factory.ts +62 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FileReaderService,
|
|
3
|
+
FileSource,
|
|
4
|
+
SliceResult,
|
|
5
|
+
} from "@uploadista/client-core";
|
|
6
|
+
import type { ReactNativeUploadInput } from "@/types/upload-input";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* React Native-specific implementation of FileReaderService
|
|
10
|
+
* Handles Blob, File, and URI-based file inputs
|
|
11
|
+
*/
|
|
12
|
+
export function createReactNativeFileReaderService(): FileReaderService<ReactNativeUploadInput> {
|
|
13
|
+
return {
|
|
14
|
+
async openFile(input, _chunkSize: number): Promise<FileSource> {
|
|
15
|
+
// Handle Blob/File objects
|
|
16
|
+
if (input instanceof Blob) {
|
|
17
|
+
return createBlobFileSource(input);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Handle URI strings or URI objects
|
|
21
|
+
if (
|
|
22
|
+
typeof input === "string" ||
|
|
23
|
+
(input && typeof input === "object" && "uri" in input)
|
|
24
|
+
) {
|
|
25
|
+
const uri =
|
|
26
|
+
typeof input === "string" ? input : (input as { uri: string }).uri;
|
|
27
|
+
return createUriFileSource(uri);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Unsupported file input type for React Native. Expected Blob, File, URI string, or {uri: string}",
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a FileSource from a Blob object
|
|
39
|
+
*/
|
|
40
|
+
function createBlobFileSource(blob: Blob): FileSource {
|
|
41
|
+
return {
|
|
42
|
+
input: blob,
|
|
43
|
+
size: blob.size,
|
|
44
|
+
async slice(start: number, end: number): Promise<SliceResult> {
|
|
45
|
+
const chunk = blob.slice(start, end);
|
|
46
|
+
|
|
47
|
+
// React Native Blob may not have arrayBuffer() method
|
|
48
|
+
let arrayBuffer: ArrayBuffer;
|
|
49
|
+
if (typeof chunk.arrayBuffer === "function") {
|
|
50
|
+
arrayBuffer = await chunk.arrayBuffer();
|
|
51
|
+
} else {
|
|
52
|
+
// Fallback: use FileReader for React Native
|
|
53
|
+
arrayBuffer = await blobToArrayBuffer(chunk);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const done = end >= blob.size;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
done,
|
|
60
|
+
value: new Uint8Array(arrayBuffer),
|
|
61
|
+
size: chunk.size,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
close() {
|
|
65
|
+
// No cleanup needed for Blob
|
|
66
|
+
},
|
|
67
|
+
name: null,
|
|
68
|
+
type: null,
|
|
69
|
+
lastModified: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert Blob to ArrayBuffer using FileReader (fallback for React Native)
|
|
75
|
+
*/
|
|
76
|
+
function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const reader = new FileReader();
|
|
79
|
+
reader.onload = () => {
|
|
80
|
+
if (reader.result instanceof ArrayBuffer) {
|
|
81
|
+
resolve(reader.result);
|
|
82
|
+
} else {
|
|
83
|
+
reject(new Error("FileReader result is not an ArrayBuffer"));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
reader.onerror = () => reject(reader.error);
|
|
87
|
+
reader.readAsArrayBuffer(blob);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a FileSource from a URI
|
|
93
|
+
* This is a simplified implementation - you may need to use react-native-fs
|
|
94
|
+
* or another library for more advanced file operations
|
|
95
|
+
*/
|
|
96
|
+
function createUriFileSource(uri: string): FileSource {
|
|
97
|
+
// For URIs, we need to fetch the file first
|
|
98
|
+
// This implementation assumes the URI can be fetched as a blob
|
|
99
|
+
let cachedBlob: Blob | null = null;
|
|
100
|
+
let cachedSize: number | null = null;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
input: uri,
|
|
104
|
+
size: cachedSize,
|
|
105
|
+
async slice(start: number, end: number): Promise<SliceResult> {
|
|
106
|
+
// Fetch the blob if not cached
|
|
107
|
+
if (!cachedBlob) {
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(uri);
|
|
110
|
+
cachedBlob = await response.blob();
|
|
111
|
+
cachedSize = cachedBlob.size;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new Error(`Failed to fetch file from URI ${uri}: ${error}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const chunk = cachedBlob.slice(start, end);
|
|
118
|
+
|
|
119
|
+
// React Native Blob may not have arrayBuffer() method
|
|
120
|
+
let arrayBuffer: ArrayBuffer;
|
|
121
|
+
if (typeof chunk.arrayBuffer === "function") {
|
|
122
|
+
arrayBuffer = await chunk.arrayBuffer();
|
|
123
|
+
} else {
|
|
124
|
+
arrayBuffer = await blobToArrayBuffer(chunk);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const done = end >= cachedBlob.size;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
done,
|
|
131
|
+
value: new Uint8Array(arrayBuffer),
|
|
132
|
+
size: chunk.size,
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
close() {
|
|
136
|
+
// Clear cached blob
|
|
137
|
+
cachedBlob = null;
|
|
138
|
+
cachedSize = null;
|
|
139
|
+
},
|
|
140
|
+
lastModified: null,
|
|
141
|
+
name: uri,
|
|
142
|
+
type: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionHealth,
|
|
3
|
+
ConnectionMetrics,
|
|
4
|
+
ConnectionPoolConfig,
|
|
5
|
+
DetailedConnectionMetrics,
|
|
6
|
+
HeadersLike,
|
|
7
|
+
Http2Info,
|
|
8
|
+
HttpClient,
|
|
9
|
+
HttpRequestOptions,
|
|
10
|
+
HttpResponse,
|
|
11
|
+
} from "@uploadista/client-core";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* React Native-specific implementation of HttpClient using fetch API
|
|
15
|
+
* React Native's fetch is similar to browser fetch but may have different performance characteristics
|
|
16
|
+
*/
|
|
17
|
+
export function createReactNativeHttpClient(
|
|
18
|
+
config?: ConnectionPoolConfig,
|
|
19
|
+
): HttpClient {
|
|
20
|
+
return new ReactNativeHttpClient(config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* React Native HTTP client implementation
|
|
25
|
+
*/
|
|
26
|
+
class ReactNativeHttpClient implements HttpClient {
|
|
27
|
+
private config: ConnectionPoolConfig;
|
|
28
|
+
private metrics: ConnectionMetrics;
|
|
29
|
+
private connectionTimes: number[] = [];
|
|
30
|
+
private requestCount = 0;
|
|
31
|
+
private connectionCount = 0;
|
|
32
|
+
private errorCount = 0;
|
|
33
|
+
private timeoutCount = 0;
|
|
34
|
+
private retryCount = 0;
|
|
35
|
+
private startTime = Date.now();
|
|
36
|
+
|
|
37
|
+
constructor(config: ConnectionPoolConfig = {}) {
|
|
38
|
+
this.config = {
|
|
39
|
+
maxConnectionsPerHost: config.maxConnectionsPerHost ?? 6,
|
|
40
|
+
connectionTimeout: config.connectionTimeout ?? 30000,
|
|
41
|
+
keepAliveTimeout: config.keepAliveTimeout ?? 60000,
|
|
42
|
+
enableHttp2: config.enableHttp2 ?? false, // HTTP/2 not supported in RN
|
|
43
|
+
retryOnConnectionError: config.retryOnConnectionError ?? true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.metrics = {
|
|
47
|
+
activeConnections: 0,
|
|
48
|
+
totalConnections: 0,
|
|
49
|
+
reuseRate: 0,
|
|
50
|
+
averageConnectionTime: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async request(
|
|
55
|
+
url: string,
|
|
56
|
+
options: HttpRequestOptions = {},
|
|
57
|
+
): Promise<HttpResponse> {
|
|
58
|
+
this.requestCount++;
|
|
59
|
+
|
|
60
|
+
const fetchOptions: RequestInit = {
|
|
61
|
+
method: options.method || "GET",
|
|
62
|
+
headers: options.headers,
|
|
63
|
+
body: options.body as any, // React Native's BodyInit type
|
|
64
|
+
signal: options.signal as AbortSignal | undefined,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Add credentials if specified
|
|
68
|
+
if (options.credentials) {
|
|
69
|
+
fetchOptions.credentials = options.credentials as any; // React Native's RequestCredentials type
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Handle timeout
|
|
76
|
+
const timeoutPromise = options.timeout
|
|
77
|
+
? new Promise<never>((_, reject) => {
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
this.timeoutCount++;
|
|
80
|
+
reject(new Error(`Request timeout after ${options.timeout}ms`));
|
|
81
|
+
}, options.timeout);
|
|
82
|
+
})
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
const fetchPromise = fetch(url, fetchOptions);
|
|
86
|
+
|
|
87
|
+
const response = timeoutPromise
|
|
88
|
+
? await Promise.race([fetchPromise, timeoutPromise])
|
|
89
|
+
: await fetchPromise;
|
|
90
|
+
|
|
91
|
+
const connectionTime = Date.now() - startTime;
|
|
92
|
+
this.connectionTimes.push(connectionTime);
|
|
93
|
+
this.connectionCount++;
|
|
94
|
+
this.metrics.totalConnections++;
|
|
95
|
+
this.updateMetrics();
|
|
96
|
+
|
|
97
|
+
// Convert fetch Response to HttpResponse
|
|
98
|
+
return this.adaptResponse(response);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.errorCount++;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private adaptResponse(response: Response): HttpResponse {
|
|
106
|
+
const headers: HeadersLike = {
|
|
107
|
+
get: (name: string) => response.headers.get(name),
|
|
108
|
+
has: (name: string) => response.headers.has(name),
|
|
109
|
+
forEach: (callback: (value: string, name: string) => void) => {
|
|
110
|
+
response.headers.forEach(callback);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
status: response.status,
|
|
116
|
+
statusText: response.statusText,
|
|
117
|
+
headers,
|
|
118
|
+
ok: response.ok,
|
|
119
|
+
json: () => response.json(),
|
|
120
|
+
text: () => response.text(),
|
|
121
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private updateMetrics(): void {
|
|
126
|
+
if (this.connectionTimes.length > 0) {
|
|
127
|
+
const sum = this.connectionTimes.reduce((a, b) => a + b, 0);
|
|
128
|
+
this.metrics.averageConnectionTime = sum / this.connectionTimes.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Estimate reuse rate based on connection times
|
|
132
|
+
// Faster connections are likely reused
|
|
133
|
+
const fastConnections = this.connectionTimes.filter(
|
|
134
|
+
(time) => time < 100,
|
|
135
|
+
).length;
|
|
136
|
+
this.metrics.reuseRate =
|
|
137
|
+
this.connectionTimes.length > 0
|
|
138
|
+
? fastConnections / this.connectionTimes.length
|
|
139
|
+
: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getMetrics(): ConnectionMetrics {
|
|
143
|
+
return { ...this.metrics };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getDetailedMetrics(): DetailedConnectionMetrics {
|
|
147
|
+
const uptime = Date.now() - this.startTime;
|
|
148
|
+
const requestsPerSecond =
|
|
149
|
+
uptime > 0 ? this.requestCount / (uptime / 1000) : 0;
|
|
150
|
+
const errorRate =
|
|
151
|
+
this.requestCount > 0 ? this.errorCount / this.requestCount : 0;
|
|
152
|
+
|
|
153
|
+
const fastConnections = this.connectionTimes.filter(
|
|
154
|
+
(time) => time < 100,
|
|
155
|
+
).length;
|
|
156
|
+
const slowConnections = this.connectionTimes.length - fastConnections;
|
|
157
|
+
|
|
158
|
+
const health = this.calculateHealth(errorRate);
|
|
159
|
+
|
|
160
|
+
const http2Info: Http2Info = {
|
|
161
|
+
supported: false, // React Native doesn't support HTTP/2
|
|
162
|
+
detected: false,
|
|
163
|
+
version: "h1.1",
|
|
164
|
+
multiplexingActive: false,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...this.metrics,
|
|
169
|
+
health,
|
|
170
|
+
requestsPerSecond,
|
|
171
|
+
errorRate,
|
|
172
|
+
timeouts: this.timeoutCount,
|
|
173
|
+
retries: this.retryCount,
|
|
174
|
+
fastConnections,
|
|
175
|
+
slowConnections,
|
|
176
|
+
http2Info,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private calculateHealth(errorRate: number): ConnectionHealth {
|
|
181
|
+
let status: "healthy" | "degraded" | "poor";
|
|
182
|
+
let score: number;
|
|
183
|
+
const issues: string[] = [];
|
|
184
|
+
const recommendations: string[] = [];
|
|
185
|
+
|
|
186
|
+
if (errorRate > 0.1) {
|
|
187
|
+
status = "poor";
|
|
188
|
+
score = 30;
|
|
189
|
+
issues.push(`High error rate: ${(errorRate * 100).toFixed(1)}%`);
|
|
190
|
+
recommendations.push("Check network connectivity");
|
|
191
|
+
} else if (errorRate > 0.05) {
|
|
192
|
+
status = "degraded";
|
|
193
|
+
score = 60;
|
|
194
|
+
issues.push(`Moderate error rate: ${(errorRate * 100).toFixed(1)}%`);
|
|
195
|
+
recommendations.push("Monitor connection stability");
|
|
196
|
+
} else {
|
|
197
|
+
status = "healthy";
|
|
198
|
+
score = 100;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.metrics.averageConnectionTime > 1000) {
|
|
202
|
+
issues.push(
|
|
203
|
+
`Slow connections: ${this.metrics.averageConnectionTime.toFixed(0)}ms avg`,
|
|
204
|
+
);
|
|
205
|
+
recommendations.push("Check network conditions");
|
|
206
|
+
score = Math.min(score, 70);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { status, score, issues, recommendations };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
reset(): void {
|
|
213
|
+
this.metrics = {
|
|
214
|
+
activeConnections: 0,
|
|
215
|
+
totalConnections: 0,
|
|
216
|
+
reuseRate: 0,
|
|
217
|
+
averageConnectionTime: 0,
|
|
218
|
+
};
|
|
219
|
+
this.connectionTimes = [];
|
|
220
|
+
this.requestCount = 0;
|
|
221
|
+
this.connectionCount = 0;
|
|
222
|
+
this.errorCount = 0;
|
|
223
|
+
this.timeoutCount = 0;
|
|
224
|
+
this.retryCount = 0;
|
|
225
|
+
this.startTime = Date.now();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async close(): Promise<void> {
|
|
229
|
+
// React Native fetch doesn't require explicit connection closing
|
|
230
|
+
this.reset();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async warmupConnections(urls: string[]): Promise<void> {
|
|
234
|
+
// Warmup by making HEAD requests to the URLs
|
|
235
|
+
const promises = urls.map((url) =>
|
|
236
|
+
this.request(url, { method: "HEAD" }).catch(() => {
|
|
237
|
+
// Ignore warmup errors
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
await Promise.all(promises);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IdGenerationService } from "@uploadista/client-core";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Native-specific implementation of IdGenerationService using uuid library
|
|
6
|
+
* crypto.randomUUID() is not available in React Native, so we use the uuid library
|
|
7
|
+
*/
|
|
8
|
+
export function createReactNativeIdGenerationService(): IdGenerationService {
|
|
9
|
+
return {
|
|
10
|
+
generate(): string {
|
|
11
|
+
return uuidv4();
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native service implementations for Uploadista client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createReactNativeBase64Service } from "./base64-service";
|
|
6
|
+
export {
|
|
7
|
+
createReactNativeServices,
|
|
8
|
+
type ReactNativeServiceOptions,
|
|
9
|
+
} from "./create-react-native-services";
|
|
10
|
+
|
|
11
|
+
export { createReactNativeFileReaderService } from "./file-reader-service";
|
|
12
|
+
|
|
13
|
+
export { createReactNativeHttpClient } from "./http-client";
|
|
14
|
+
export { createReactNativeIdGenerationService } from "./id-generation-service";
|
|
15
|
+
export { createAsyncStorageService } from "./storage-service";
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CameraOptions,
|
|
3
|
+
FileInfo,
|
|
4
|
+
FilePickResult,
|
|
5
|
+
FileSystemProvider,
|
|
6
|
+
PickerOptions,
|
|
7
|
+
} from "@uploadista/react-native-core";
|
|
8
|
+
|
|
9
|
+
import * as DocumentPicker from "react-native-document-picker";
|
|
10
|
+
import * as ImagePicker from "react-native-image-picker";
|
|
11
|
+
import RNFetchBlob from "rn-fetch-blob";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* File system provider implementation for bare React Native environment
|
|
15
|
+
* Uses react-native-image-picker, react-native-fs, and native APIs
|
|
16
|
+
*/
|
|
17
|
+
export class NativeFileSystemProvider implements FileSystemProvider {
|
|
18
|
+
async pickDocument(options?: PickerOptions): Promise<FilePickResult> {
|
|
19
|
+
try {
|
|
20
|
+
const result = await DocumentPicker.pick({
|
|
21
|
+
type: options?.allowedTypes || [DocumentPicker.types.allFiles],
|
|
22
|
+
...(options?.allowMultiple && { isMultiple: true }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const file = Array.isArray(result) ? result[0] : result;
|
|
26
|
+
|
|
27
|
+
if (!file) {
|
|
28
|
+
throw new Error("No document selected");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
uri: file.uri,
|
|
33
|
+
name: file.name || "document",
|
|
34
|
+
size: file.size || 0,
|
|
35
|
+
mimeType: file.type || undefined,
|
|
36
|
+
localPath: file.fileCopyUri || undefined,
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (
|
|
40
|
+
error instanceof Error &&
|
|
41
|
+
(error.message.includes("cancelled") ||
|
|
42
|
+
error.message.includes("Cancelled"))
|
|
43
|
+
) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Failed to pick document: ${error instanceof Error ? error.message : String(error)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async pickImage(options?: PickerOptions): Promise<FilePickResult> {
|
|
53
|
+
try {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
ImagePicker.launchImageLibrary(
|
|
56
|
+
{
|
|
57
|
+
mediaType: "photo",
|
|
58
|
+
selectionLimit: options?.allowMultiple ? 0 : 1,
|
|
59
|
+
quality: 1,
|
|
60
|
+
// biome-ignore lint/suspicious/noExplicitAny: react-native-image-picker type mismatch
|
|
61
|
+
} as any,
|
|
62
|
+
(response: unknown) => {
|
|
63
|
+
const res = response as {
|
|
64
|
+
didCancel?: boolean;
|
|
65
|
+
errorCode?: string;
|
|
66
|
+
assets?: Array<{
|
|
67
|
+
uri: string;
|
|
68
|
+
fileName?: string;
|
|
69
|
+
fileSize?: number;
|
|
70
|
+
type?: string;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (res.didCancel) {
|
|
75
|
+
reject(new Error("Image picker was cancelled"));
|
|
76
|
+
} else if (res.errorCode) {
|
|
77
|
+
reject(new Error(`Image picker error: ${res.errorCode}`));
|
|
78
|
+
} else if (res.assets && res.assets.length > 0) {
|
|
79
|
+
const asset = res.assets[0];
|
|
80
|
+
if (asset) {
|
|
81
|
+
resolve({
|
|
82
|
+
uri: asset.uri,
|
|
83
|
+
name: asset.fileName || `image-${Date.now()}.jpg`,
|
|
84
|
+
size: asset.fileSize || 0,
|
|
85
|
+
mimeType: asset.type || "image/jpeg",
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
reject(new Error("No image selected"));
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
reject(new Error("No image selected"));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Failed to pick image: ${error instanceof Error ? error.message : String(error)}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async pickVideo(options?: PickerOptions): Promise<FilePickResult> {
|
|
104
|
+
try {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
ImagePicker.launchImageLibrary(
|
|
107
|
+
{
|
|
108
|
+
mediaType: "video",
|
|
109
|
+
selectionLimit: options?.allowMultiple ? 0 : 1,
|
|
110
|
+
},
|
|
111
|
+
(response: unknown) => {
|
|
112
|
+
const res = response as {
|
|
113
|
+
didCancel?: boolean;
|
|
114
|
+
errorCode?: string;
|
|
115
|
+
assets?: Array<{
|
|
116
|
+
uri: string;
|
|
117
|
+
fileName?: string;
|
|
118
|
+
fileSize?: number;
|
|
119
|
+
type?: string;
|
|
120
|
+
}>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (res.didCancel) {
|
|
124
|
+
reject(new Error("Video picker was cancelled"));
|
|
125
|
+
} else if (res.errorCode) {
|
|
126
|
+
reject(new Error(`Video picker error: ${res.errorCode}`));
|
|
127
|
+
} else if (res.assets && res.assets.length > 0) {
|
|
128
|
+
const asset = res.assets[0];
|
|
129
|
+
if (asset) {
|
|
130
|
+
resolve({
|
|
131
|
+
uri: asset.uri,
|
|
132
|
+
name: asset.fileName || `video-${Date.now()}.mp4`,
|
|
133
|
+
size: asset.fileSize || 0,
|
|
134
|
+
mimeType: asset.type || "video/mp4",
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
reject(new Error("No video selected"));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
reject(new Error("No video selected"));
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Failed to pick video: ${error instanceof Error ? error.message : String(error)}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async pickCamera(options?: CameraOptions): Promise<FilePickResult> {
|
|
153
|
+
try {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
ImagePicker.launchCamera(
|
|
156
|
+
{
|
|
157
|
+
mediaType: "photo",
|
|
158
|
+
cameraType: options?.cameraType === "front" ? "front" : "back",
|
|
159
|
+
quality: options?.quality ?? 1,
|
|
160
|
+
// biome-ignore lint/suspicious/noExplicitAny: react-native-image-picker type mismatch
|
|
161
|
+
} as any,
|
|
162
|
+
(response: unknown) => {
|
|
163
|
+
const res = response as {
|
|
164
|
+
didCancel?: boolean;
|
|
165
|
+
errorCode?: string;
|
|
166
|
+
assets?: Array<{
|
|
167
|
+
uri: string;
|
|
168
|
+
fileName?: string;
|
|
169
|
+
fileSize?: number;
|
|
170
|
+
type?: string;
|
|
171
|
+
}>;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (res.didCancel) {
|
|
175
|
+
reject(new Error("Camera was cancelled"));
|
|
176
|
+
} else if (res.errorCode) {
|
|
177
|
+
reject(new Error(`Camera error: ${res.errorCode}`));
|
|
178
|
+
} else if (res.assets && res.assets.length > 0) {
|
|
179
|
+
const asset = res.assets[0];
|
|
180
|
+
if (asset) {
|
|
181
|
+
resolve({
|
|
182
|
+
uri: asset.uri,
|
|
183
|
+
name: asset.fileName || `photo-${Date.now()}.jpg`,
|
|
184
|
+
size: asset.fileSize || 0,
|
|
185
|
+
mimeType: asset.type || "image/jpeg",
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
reject(new Error("No photo captured"));
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
reject(new Error("No photo captured"));
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Failed to capture photo: ${error instanceof Error ? error.message : String(error)}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async readFile(uri: string): Promise<ArrayBuffer> {
|
|
204
|
+
try {
|
|
205
|
+
// Read file as base64
|
|
206
|
+
const base64Data = await RNFetchBlob.fs.readFile(uri, "base64");
|
|
207
|
+
|
|
208
|
+
// Convert base64 to ArrayBuffer
|
|
209
|
+
const binaryString = atob(base64Data);
|
|
210
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
211
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
212
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
213
|
+
}
|
|
214
|
+
return bytes.buffer;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async getDocumentUri(filePath: string): Promise<string> {
|
|
223
|
+
// In bare RN with native modules, the path is typically already a URI
|
|
224
|
+
return filePath;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getFileInfo(uri: string): Promise<FileInfo> {
|
|
228
|
+
try {
|
|
229
|
+
const stat = await RNFetchBlob.fs.stat(uri);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
uri,
|
|
233
|
+
name: stat.filename || uri.split("/").pop() || "unknown",
|
|
234
|
+
size: stat.size || 0,
|
|
235
|
+
modificationTime: stat.lastModified
|
|
236
|
+
? typeof stat.lastModified === "string"
|
|
237
|
+
? parseInt(stat.lastModified, 10)
|
|
238
|
+
: stat.lastModified
|
|
239
|
+
: undefined,
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Failed to get file info: ${error instanceof Error ? error.message : String(error)}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|