@uploadista/react-native-bare 0.0.20-beta.6 → 0.0.20-beta.8
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/package.json +22 -7
- package/src/services/checksum-service.ts +14 -0
- package/src/services/create-react-native-services.ts +10 -1
- package/src/services/file-reader-service.ts +1 -1
- package/src/services/fingerprint-service.ts +114 -0
- package/src/services/storage-service.ts +21 -3
- package/src/services/websocket-factory.ts +20 -13
- package/src/utils/hash-util.ts +60 -0
package/package.json
CHANGED
|
@@ -2,30 +2,45 @@
|
|
|
2
2
|
"name": "@uploadista/react-native-bare",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Bare React Native Client for Uploadista",
|
|
5
|
-
"version": "0.0.20-beta.
|
|
5
|
+
"version": "0.0.20-beta.8",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@uploadista/client-core": "0.0.20-beta.
|
|
13
|
-
"@uploadista/react-native-core": "0.0.20-beta.
|
|
12
|
+
"@uploadista/client-core": "0.0.20-beta.8",
|
|
13
|
+
"@uploadista/react-native-core": "0.0.20-beta.8"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
+
"js-base64": ">=3.7.0",
|
|
17
|
+
"uuid": ">=13.0.0",
|
|
16
18
|
"react": ">=16.8.0",
|
|
17
19
|
"react-native": ">=0.71.0",
|
|
18
20
|
"@react-native-documents/picker": ">=10.0.0",
|
|
19
21
|
"react-native-image-picker": ">=7.0.0",
|
|
20
|
-
"
|
|
22
|
+
"react-native-quick-crypto": ">=0.7.0",
|
|
23
|
+
"rn-fetch-blob": ">=0.12.0",
|
|
24
|
+
"@react-native-async-storage/async-storage": ">=1.19.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@react-native-documents/picker": {
|
|
28
|
+
"optional": true
|
|
29
|
+
},
|
|
30
|
+
"react-native-image-picker": {
|
|
31
|
+
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"@react-native-async-storage/async-storage": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
21
36
|
},
|
|
22
37
|
"devDependencies": {
|
|
23
38
|
"@types/react": ">=18.0.0",
|
|
24
|
-
"tsdown": "0.
|
|
25
|
-
"@uploadista/typescript-config": "0.0.20-beta.
|
|
39
|
+
"tsdown": "0.18.0",
|
|
40
|
+
"@uploadista/typescript-config": "0.0.20-beta.8"
|
|
26
41
|
},
|
|
27
42
|
"scripts": {
|
|
28
|
-
"build": "tsdown",
|
|
43
|
+
"build": "tsc --noEmit && tsdown",
|
|
29
44
|
"format": "biome format --write ./src",
|
|
30
45
|
"lint": "biome lint --write ./src",
|
|
31
46
|
"check": "biome check --write ./src"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChecksumService } from "@uploadista/client-core";
|
|
2
|
+
import { computeUint8ArraySha256 } from "../utils/hash-util";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a ChecksumService for Expo environments
|
|
6
|
+
* Computes SHA-256 checksums of file data using Web Crypto API
|
|
7
|
+
*/
|
|
8
|
+
export function createReactNativeChecksumService(): ChecksumService {
|
|
9
|
+
return {
|
|
10
|
+
computeChecksum: async (data: Uint8Array<ArrayBuffer>) => {
|
|
11
|
+
return computeUint8ArraySha256(data);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -3,12 +3,15 @@ import {
|
|
|
3
3
|
createInMemoryStorageService,
|
|
4
4
|
type ServiceContainer,
|
|
5
5
|
} from "@uploadista/client-core";
|
|
6
|
-
import type { ReactNativeUploadInput } from "
|
|
6
|
+
import type { ReactNativeUploadInput } from "@uploadista/react-native-core";
|
|
7
7
|
import { createReactNativeAbortControllerFactory } from "./abort-controller-factory";
|
|
8
8
|
import { createReactNativeBase64Service } from "./base64-service";
|
|
9
|
+
import { createReactNativeChecksumService } from "./checksum-service";
|
|
9
10
|
import { createReactNativeFileReaderService } from "./file-reader-service";
|
|
11
|
+
import { createReactNativeFingerprintService } from "./fingerprint-service";
|
|
10
12
|
import { createReactNativeHttpClient } from "./http-client";
|
|
11
13
|
import { createReactNativeIdGenerationService } from "./id-generation-service";
|
|
14
|
+
import { createReactNativePlatformService } from "./platform-service";
|
|
12
15
|
import { createAsyncStorageService } from "./storage-service";
|
|
13
16
|
import { createReactNativeWebSocketFactory } from "./websocket-factory";
|
|
14
17
|
|
|
@@ -63,6 +66,9 @@ export function createReactNativeServices(
|
|
|
63
66
|
const base64 = createReactNativeBase64Service();
|
|
64
67
|
const websocket = createReactNativeWebSocketFactory();
|
|
65
68
|
const abortController = createReactNativeAbortControllerFactory();
|
|
69
|
+
const platform = createReactNativePlatformService();
|
|
70
|
+
const checksumService = createReactNativeChecksumService();
|
|
71
|
+
const fingerprintService = createReactNativeFingerprintService();
|
|
66
72
|
|
|
67
73
|
return {
|
|
68
74
|
storage,
|
|
@@ -70,6 +76,9 @@ export function createReactNativeServices(
|
|
|
70
76
|
httpClient,
|
|
71
77
|
fileReader,
|
|
72
78
|
base64,
|
|
79
|
+
platform,
|
|
80
|
+
checksumService,
|
|
81
|
+
fingerprintService,
|
|
73
82
|
websocket,
|
|
74
83
|
abortController,
|
|
75
84
|
};
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
FileSource,
|
|
4
4
|
SliceResult,
|
|
5
5
|
} from "@uploadista/client-core";
|
|
6
|
-
import type { ReactNativeUploadInput } from "
|
|
6
|
+
import type { ReactNativeUploadInput } from "@uploadista/react-native-core";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* React Native-specific implementation of FileReaderService
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { FingerprintService } from "@uploadista/client-core";
|
|
2
|
+
import type { ReactNativeUploadInput } from "@uploadista/react-native-core";
|
|
3
|
+
import { createHash } from "react-native-quick-crypto";
|
|
4
|
+
import { computeblobSha256 } from "../utils/hash-util";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a FingerprintService for bare React Native environments
|
|
8
|
+
* Computes file fingerprints using SHA-256 hashing
|
|
9
|
+
* Supports Blob, File, and URI-based inputs
|
|
10
|
+
*/
|
|
11
|
+
export function createReactNativeFingerprintService(): FingerprintService<ReactNativeUploadInput> {
|
|
12
|
+
return {
|
|
13
|
+
computeFingerprint: async (input, _endpoint) => {
|
|
14
|
+
// Handle Blob/File objects directly
|
|
15
|
+
if (input instanceof Blob) {
|
|
16
|
+
return computeblobSha256(input);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// For URI inputs (string or {uri: string}), we need to convert to Blob first
|
|
20
|
+
if (
|
|
21
|
+
typeof input === "string" ||
|
|
22
|
+
(input && typeof input === "object" && "uri" in input)
|
|
23
|
+
) {
|
|
24
|
+
const uri =
|
|
25
|
+
typeof input === "string" ? input : (input as { uri: string }).uri;
|
|
26
|
+
return computeFingerprintFromUri(uri);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Unsupported file input type for fingerprinting. Expected Blob, File, URI string, or {uri: string}",
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute fingerprint from a file URI
|
|
38
|
+
* Uses rn-fetch-blob to read the file and compute its SHA-256 hash
|
|
39
|
+
*/
|
|
40
|
+
async function computeFingerprintFromUri(uri: string): Promise<string> {
|
|
41
|
+
try {
|
|
42
|
+
// Use rn-fetch-blob to read the file as base64
|
|
43
|
+
const RNFetchBlob = getRNFetchBlob();
|
|
44
|
+
|
|
45
|
+
// Normalize URI path for rn-fetch-blob
|
|
46
|
+
const normalizedPath = normalizeUri(uri);
|
|
47
|
+
|
|
48
|
+
// Check if file exists
|
|
49
|
+
const exists = await RNFetchBlob.fs.exists(normalizedPath);
|
|
50
|
+
if (!exists) {
|
|
51
|
+
throw new Error(`File does not exist at URI: ${uri}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read the entire file as base64
|
|
55
|
+
const base64String = await RNFetchBlob.fs.readFile(normalizedPath, "base64");
|
|
56
|
+
|
|
57
|
+
// Convert base64 to Uint8Array
|
|
58
|
+
const uint8Array = base64ToUint8Array(base64String);
|
|
59
|
+
|
|
60
|
+
// Compute SHA-256 hash using react-native-quick-crypto
|
|
61
|
+
const hash = createHash("sha256");
|
|
62
|
+
hash.update(uint8Array);
|
|
63
|
+
return hash.digest("hex");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Failed to compute fingerprint from URI ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalize URI for rn-fetch-blob
|
|
73
|
+
* Strips file:// prefix and handles platform-specific paths
|
|
74
|
+
*/
|
|
75
|
+
function normalizeUri(uri: string): string {
|
|
76
|
+
// Remove file:// prefix if present
|
|
77
|
+
if (uri.startsWith("file://")) {
|
|
78
|
+
return uri.substring(7);
|
|
79
|
+
}
|
|
80
|
+
return uri;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get rn-fetch-blob module
|
|
85
|
+
* This allows the service to work even if rn-fetch-blob is not installed
|
|
86
|
+
*/
|
|
87
|
+
function getRNFetchBlob() {
|
|
88
|
+
try {
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
90
|
+
return require("rn-fetch-blob").default;
|
|
91
|
+
} catch (_error) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"rn-fetch-blob is required for URI-based fingerprinting. " +
|
|
94
|
+
"Please install it with: npm install rn-fetch-blob",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert base64 string to Uint8Array
|
|
101
|
+
* Uses js-base64 library for cross-platform compatibility
|
|
102
|
+
*/
|
|
103
|
+
function base64ToUint8Array(base64: string): Uint8Array {
|
|
104
|
+
// Use js-base64 for decoding (works in all environments)
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
106
|
+
const { fromBase64 } = require("js-base64");
|
|
107
|
+
const binaryString = fromBase64(base64);
|
|
108
|
+
|
|
109
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
110
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
111
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
112
|
+
}
|
|
113
|
+
return bytes;
|
|
114
|
+
}
|
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
1
|
import type { StorageService } from "@uploadista/client-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get AsyncStorage module dynamically
|
|
5
|
+
* This allows the service to work even if AsyncStorage is not installed
|
|
6
|
+
*/
|
|
7
|
+
function getAsyncStorage() {
|
|
8
|
+
try {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
return require("@react-native-async-storage/async-storage").default;
|
|
11
|
+
} catch (_error) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"@react-native-async-storage/async-storage is required for persistent storage. " +
|
|
14
|
+
"Please install it with: npm install @react-native-async-storage/async-storage",
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
|
-
*
|
|
20
|
+
* React Native-specific implementation of StorageService using AsyncStorage
|
|
5
21
|
* AsyncStorage is provided as an optional peer dependency and must be installed separately
|
|
6
22
|
*/
|
|
7
23
|
export function createAsyncStorageService(): StorageService {
|
|
24
|
+
const AsyncStorage = getAsyncStorage();
|
|
25
|
+
|
|
8
26
|
const findEntries = async (
|
|
9
27
|
prefix: string,
|
|
10
28
|
): Promise<Record<string, string>> => {
|
|
11
29
|
const results: Record<string, string> = {};
|
|
12
30
|
|
|
13
31
|
const keys = await AsyncStorage.getAllKeys();
|
|
14
|
-
for (const key
|
|
32
|
+
for (const key of keys) {
|
|
15
33
|
if (key.startsWith(prefix)) {
|
|
16
34
|
const item = await AsyncStorage.getItem(key);
|
|
17
35
|
if (item) {
|
|
@@ -10,35 +10,42 @@ class ReactNativeWebSocket implements WebSocketLike {
|
|
|
10
10
|
readonly CLOSING = 2;
|
|
11
11
|
readonly CLOSED = 3;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
onmessage: ((event: unknown) => void) | null = null;
|
|
13
|
+
onopen: (() => void) | null = null;
|
|
14
|
+
onclose: ((event: { code: number; reason: string }) => void) | null = null;
|
|
15
|
+
onerror: ((event: { message: string }) => void) | null = null;
|
|
16
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
18
17
|
|
|
19
18
|
private native: WebSocket;
|
|
20
19
|
|
|
20
|
+
get readyState(): number {
|
|
21
|
+
return this.native.readyState;
|
|
22
|
+
}
|
|
23
|
+
|
|
21
24
|
constructor(url: string) {
|
|
22
25
|
this.native = new WebSocket(url);
|
|
23
|
-
this.readyState = this.native.readyState;
|
|
24
26
|
|
|
25
27
|
// Proxy event handlers
|
|
26
|
-
this.native.onopen = (
|
|
27
|
-
this.
|
|
28
|
-
this.onopen?.(event);
|
|
28
|
+
this.native.onopen = () => {
|
|
29
|
+
this.onopen?.();
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
this.native.onclose = (event) => {
|
|
32
|
-
this.
|
|
33
|
-
|
|
33
|
+
this.onclose?.({
|
|
34
|
+
code: (event as CloseEvent).code ?? 1000,
|
|
35
|
+
reason: (event as CloseEvent).reason ?? "",
|
|
36
|
+
});
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
this.native.onerror = (event) => {
|
|
37
|
-
this.onerror?.(
|
|
40
|
+
this.onerror?.({
|
|
41
|
+
message: (event as ErrorEvent).message ?? "WebSocket error",
|
|
42
|
+
});
|
|
38
43
|
};
|
|
39
44
|
|
|
40
45
|
this.native.onmessage = (event) => {
|
|
41
|
-
this.onmessage?.(
|
|
46
|
+
this.onmessage?.({
|
|
47
|
+
data: (event as MessageEvent).data as string,
|
|
48
|
+
});
|
|
42
49
|
};
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "react-native-quick-crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute SHA-256 checksum using Node.js crypto API
|
|
5
|
+
* Compatible with bare React Native environments via react-native-quick-crypto polyfill
|
|
6
|
+
*
|
|
7
|
+
* @param data - Uint8Array to hash
|
|
8
|
+
* @returns Promise that resolves to hex-encoded SHA-256 checksum
|
|
9
|
+
*/
|
|
10
|
+
export async function computeUint8ArraySha256(
|
|
11
|
+
data: Uint8Array,
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
try {
|
|
14
|
+
const hash = createHash("sha256");
|
|
15
|
+
hash.update(data);
|
|
16
|
+
return hash.digest("hex");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Failed to compute checksum: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compute SHA-256 checksum of a Blob using Node.js crypto API
|
|
26
|
+
* Compatible with bare React Native environments via react-native-quick-crypto polyfill
|
|
27
|
+
*
|
|
28
|
+
* @param blob - Blob to hash
|
|
29
|
+
* @returns Promise that resolves to hex-encoded SHA-256 checksum
|
|
30
|
+
*/
|
|
31
|
+
export async function computeblobSha256(blob: Blob): Promise<string> {
|
|
32
|
+
try {
|
|
33
|
+
// Convert Blob to Uint8Array using FileReader for compatibility
|
|
34
|
+
const uint8Array = await blobToUint8Array(blob);
|
|
35
|
+
return computeUint8ArraySha256(uint8Array);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Failed to compute file checksum: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert Blob to Uint8Array using FileReader
|
|
45
|
+
* Works in React Native environments
|
|
46
|
+
*/
|
|
47
|
+
async function blobToUint8Array(blob: Blob): Promise<Uint8Array> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const reader = new FileReader();
|
|
50
|
+
reader.onload = () => {
|
|
51
|
+
if (reader.result instanceof ArrayBuffer) {
|
|
52
|
+
resolve(new Uint8Array(reader.result));
|
|
53
|
+
} else {
|
|
54
|
+
reject(new Error("FileReader result is not an ArrayBuffer"));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
reader.onerror = () => reject(reader.error);
|
|
58
|
+
reader.readAsArrayBuffer(blob);
|
|
59
|
+
});
|
|
60
|
+
}
|