@switchlabs/verify-ai-react-native 1.1.1 → 2.1.0
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/README.md +15 -11
- package/lib/client/index.d.ts +0 -3
- package/lib/client/index.js +21 -7
- package/lib/components/VerifyAIScanner.d.ts +4 -7
- package/lib/components/VerifyAIScanner.js +92 -17
- package/lib/index.d.ts +0 -2
- package/lib/index.js +0 -2
- package/lib/scanner.d.ts +2 -0
- package/lib/scanner.js +1 -0
- package/lib/storage/offlineQueue.js +26 -18
- package/lib/telemetry/TelemetryReporter.d.ts +1 -0
- package/lib/telemetry/TelemetryReporter.js +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +11 -1
- package/src/client/index.ts +23 -7
- package/src/components/VerifyAIScanner.tsx +104 -23
- package/src/index.ts +0 -4
- package/src/scanner.ts +2 -0
- package/src/storage/offlineQueue.ts +27 -18
- package/src/telemetry/TelemetryReporter.ts +3 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -4,18 +4,20 @@ React Native SDK for Verify AI photo verification.
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Core SDK (client, hooks, types):
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
npm install @switchlabs/verify-ai-react-native
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
With built-in camera scanner:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install @switchlabs/verify-ai-react-native expo-camera
|
|
16
|
+
npm install @switchlabs/verify-ai-react-native expo-camera expo-image-manipulator
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
For offline queue support, also install `@react-native-async-storage/async-storage`.
|
|
20
|
+
|
|
19
21
|
If you want on-device inference, also install `expo-file-system`,
|
|
20
22
|
`react-native-fast-tflite`, and configure `react-native-fast-tflite` for the
|
|
21
23
|
delegates you plan to use.
|
|
@@ -44,18 +46,18 @@ function ParkingScreen() {
|
|
|
44
46
|
|
|
45
47
|
## Scanner Component
|
|
46
48
|
|
|
49
|
+
The scanner is exported from a separate subpath to avoid pulling in `expo-camera` for consumers that only need the client.
|
|
50
|
+
|
|
47
51
|
```tsx
|
|
48
|
-
import { useVerifyAI
|
|
52
|
+
import { useVerifyAI } from '@switchlabs/verify-ai-react-native';
|
|
53
|
+
import { VerifyAIScanner } from '@switchlabs/verify-ai-react-native/scanner';
|
|
49
54
|
|
|
50
55
|
function ScannerScreen() {
|
|
51
|
-
const {
|
|
52
|
-
apiKey: 'vai_your_api_key',
|
|
53
|
-
enableOnDeviceML: true,
|
|
54
|
-
});
|
|
56
|
+
const { verify } = useVerifyAI({ apiKey: 'vai_your_api_key' });
|
|
55
57
|
return (
|
|
56
58
|
<VerifyAIScanner
|
|
57
|
-
onCapture={(
|
|
58
|
-
|
|
59
|
+
onCapture={(base64) =>
|
|
60
|
+
verify({ image: base64, policy: 'scooter_parking' })
|
|
59
61
|
}
|
|
60
62
|
onResult={(result) => console.log(result.is_compliant ? 'PASS' : 'FAIL')}
|
|
61
63
|
/>
|
|
@@ -65,6 +67,8 @@ function ScannerScreen() {
|
|
|
65
67
|
|
|
66
68
|
## Offline Mode
|
|
67
69
|
|
|
70
|
+
Requires `@react-native-async-storage/async-storage` to be installed.
|
|
71
|
+
|
|
68
72
|
Set `offlineMode: true` to queue transient failures (network, timeout, 429, 5xx) and retry later.
|
|
69
73
|
|
|
70
74
|
```tsx
|
package/lib/client/index.d.ts
CHANGED
|
@@ -38,9 +38,6 @@ export declare class VerifyAIClient {
|
|
|
38
38
|
* Submit a photo for AI verification using multipart/form-data.
|
|
39
39
|
* Streams the image directly from disk — avoids base64 encoding overhead.
|
|
40
40
|
*
|
|
41
|
-
* **Breaking change:** `onCapture` prop on `VerifyAIScanner` now receives
|
|
42
|
-
* an image URI instead of a base64 string.
|
|
43
|
-
*
|
|
44
41
|
* @param request - Multipart request with file URI and policy
|
|
45
42
|
* @param options - Optional verify options (e.g. idempotency key)
|
|
46
43
|
* @returns The verification result with compliance status and feedback
|
package/lib/client/index.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
2
|
+
import { SDK_VERSION } from '../version';
|
|
2
3
|
const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
|
|
3
4
|
const DEFAULT_TIMEOUT = 30000;
|
|
5
|
+
const VEHICLE_TYPE_LABELS = {
|
|
6
|
+
scooter: 'Scooter',
|
|
7
|
+
'e-bike': 'E-Bike',
|
|
8
|
+
ebike: 'E-Bike',
|
|
9
|
+
bike: 'Bike',
|
|
10
|
+
moped: 'Moped',
|
|
11
|
+
car: 'Car',
|
|
12
|
+
};
|
|
13
|
+
/** Auto-inject sdkVersion and vehicleTypeLabel into metadata. */
|
|
14
|
+
function enrichMetadata(metadata) {
|
|
15
|
+
const enriched = { ...metadata, sdkVersion: SDK_VERSION };
|
|
16
|
+
const vehicleType = enriched.vehicleType;
|
|
17
|
+
if (typeof vehicleType === 'string' && !enriched.vehicleTypeLabel) {
|
|
18
|
+
enriched.vehicleTypeLabel = VEHICLE_TYPE_LABELS[vehicleType.toLowerCase()] || vehicleType;
|
|
19
|
+
}
|
|
20
|
+
return enriched;
|
|
21
|
+
}
|
|
4
22
|
export class VerifyAIClient {
|
|
5
23
|
constructor(config) {
|
|
6
24
|
if (!config.apiKey) {
|
|
@@ -142,19 +160,17 @@ export class VerifyAIClient {
|
|
|
142
160
|
if (options?.idempotencyKey) {
|
|
143
161
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
144
162
|
}
|
|
163
|
+
const enrichedRequest = { ...request, metadata: enrichMetadata(request.metadata) };
|
|
145
164
|
return this.request('/verify', {
|
|
146
165
|
method: 'POST',
|
|
147
166
|
headers,
|
|
148
|
-
body: JSON.stringify(
|
|
167
|
+
body: JSON.stringify(enrichedRequest),
|
|
149
168
|
});
|
|
150
169
|
}
|
|
151
170
|
/**
|
|
152
171
|
* Submit a photo for AI verification using multipart/form-data.
|
|
153
172
|
* Streams the image directly from disk — avoids base64 encoding overhead.
|
|
154
173
|
*
|
|
155
|
-
* **Breaking change:** `onCapture` prop on `VerifyAIScanner` now receives
|
|
156
|
-
* an image URI instead of a base64 string.
|
|
157
|
-
*
|
|
158
174
|
* @param request - Multipart request with file URI and policy
|
|
159
175
|
* @param options - Optional verify options (e.g. idempotency key)
|
|
160
176
|
* @returns The verification result with compliance status and feedback
|
|
@@ -168,9 +184,7 @@ export class VerifyAIClient {
|
|
|
168
184
|
name: 'photo.jpg',
|
|
169
185
|
});
|
|
170
186
|
formData.append('policy', request.policy);
|
|
171
|
-
|
|
172
|
-
formData.append('metadata', JSON.stringify(request.metadata));
|
|
173
|
-
}
|
|
187
|
+
formData.append('metadata', JSON.stringify(enrichMetadata(request.metadata)));
|
|
174
188
|
if (request.provider) {
|
|
175
189
|
formData.append('provider', request.provider);
|
|
176
190
|
}
|
|
@@ -3,8 +3,8 @@ import { type ViewStyle } from 'react-native';
|
|
|
3
3
|
import type { VerificationResult, ScannerOverlayConfig } from '../types';
|
|
4
4
|
import type { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
5
5
|
export interface VerifyAIScannerProps {
|
|
6
|
-
/** Called with
|
|
7
|
-
onCapture: (
|
|
6
|
+
/** Called with base64 image data when the user captures a photo. */
|
|
7
|
+
onCapture: (base64: string) => Promise<VerificationResult | null>;
|
|
8
8
|
/** Called when a terminal verification result is reached. */
|
|
9
9
|
onResult?: (result: VerificationResult) => void;
|
|
10
10
|
/** Called when an error occurs. */
|
|
@@ -28,13 +28,10 @@ export interface VerifyAIScannerProps {
|
|
|
28
28
|
*
|
|
29
29
|
* @example
|
|
30
30
|
* ```tsx
|
|
31
|
-
* const {
|
|
32
|
-
* apiKey: 'vai_...',
|
|
33
|
-
* enableOnDeviceML: true,
|
|
34
|
-
* });
|
|
31
|
+
* const { verify } = useVerifyAI({ apiKey: 'vai_...' });
|
|
35
32
|
*
|
|
36
33
|
* <VerifyAIScanner
|
|
37
|
-
* onCapture={(
|
|
34
|
+
* onCapture={(base64) => verify({ image: base64, policy: 'scooter_parking' })}
|
|
38
35
|
* onResult={(result) => {
|
|
39
36
|
* if (result.is_compliant) navigation.navigate('Success');
|
|
40
37
|
* }}
|
|
@@ -2,8 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
3
3
|
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, } from 'react-native';
|
|
4
4
|
import { CameraView, useCameraPermissions, } from 'expo-camera';
|
|
5
|
-
import * as ImageManipulator from 'expo-image-manipulator';
|
|
6
5
|
import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
6
|
+
/** Quality used when expo-image-manipulator is not available (lower = smaller). */
|
|
7
|
+
const FALLBACK_QUALITY = 0.5;
|
|
8
|
+
/** Quality used when expo-image-manipulator IS available (resize handles size). */
|
|
9
|
+
const MANIPULATOR_QUALITY = 0.7;
|
|
10
|
+
/** Max dimension (px) on longest side when resize is available. */
|
|
11
|
+
const MAX_DIMENSION = 2048;
|
|
7
12
|
function getErrorDisplay(error, showTechnicalDetails) {
|
|
8
13
|
if (!error) {
|
|
9
14
|
return {
|
|
@@ -24,6 +29,9 @@ function getErrorDisplay(error, showTechnicalDetails) {
|
|
|
24
29
|
else if (status === 401) {
|
|
25
30
|
message = 'Verification is not configured correctly.';
|
|
26
31
|
}
|
|
32
|
+
else if (status === 413) {
|
|
33
|
+
message = 'Image is too large. Please try again — the photo will be resized automatically.';
|
|
34
|
+
}
|
|
27
35
|
else if (status === 429) {
|
|
28
36
|
message = 'Verification is temporarily unavailable. Please try again.';
|
|
29
37
|
}
|
|
@@ -50,13 +58,10 @@ function getErrorDisplay(error, showTechnicalDetails) {
|
|
|
50
58
|
*
|
|
51
59
|
* @example
|
|
52
60
|
* ```tsx
|
|
53
|
-
* const {
|
|
54
|
-
* apiKey: 'vai_...',
|
|
55
|
-
* enableOnDeviceML: true,
|
|
56
|
-
* });
|
|
61
|
+
* const { verify } = useVerifyAI({ apiKey: 'vai_...' });
|
|
57
62
|
*
|
|
58
63
|
* <VerifyAIScanner
|
|
59
|
-
* onCapture={(
|
|
64
|
+
* onCapture={(base64) => verify({ image: base64, policy: 'scooter_parking' })}
|
|
60
65
|
* onResult={(result) => {
|
|
61
66
|
* if (result.is_compliant) navigation.navigate('Success');
|
|
62
67
|
* }}
|
|
@@ -157,19 +162,89 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
157
162
|
setResult(null);
|
|
158
163
|
setLastError(null);
|
|
159
164
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
// --- Capture + best-effort resize ---
|
|
166
|
+
// Strategy: try to dynamically import expo-image-manipulator.
|
|
167
|
+
// If available → resize to MAX_DIMENSION, EXIF-normalize, return base64.
|
|
168
|
+
// If not available → use expo-camera's built-in base64 at lower quality.
|
|
169
|
+
// This keeps expo-image-manipulator as an *optional* dependency.
|
|
170
|
+
let base64;
|
|
171
|
+
let origWidth = 0;
|
|
172
|
+
let origHeight = 0;
|
|
173
|
+
let processedWidth = 0;
|
|
174
|
+
let processedHeight = 0;
|
|
175
|
+
let didResize = false;
|
|
176
|
+
let ImageManipulator = null;
|
|
177
|
+
try {
|
|
178
|
+
ImageManipulator = await import('expo-image-manipulator');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Not installed — fall back to camera-only base64 below
|
|
182
|
+
}
|
|
183
|
+
if (ImageManipulator) {
|
|
184
|
+
// Capture without base64 — ImageManipulator will produce it after resize.
|
|
185
|
+
const photo = await cameraRef.current.takePictureAsync({
|
|
186
|
+
quality: 0.8,
|
|
187
|
+
exif: false,
|
|
188
|
+
});
|
|
189
|
+
if (!photo?.uri) {
|
|
190
|
+
throw new Error('Failed to capture photo');
|
|
191
|
+
}
|
|
192
|
+
origWidth = photo.width ?? 0;
|
|
193
|
+
origHeight = photo.height ?? 0;
|
|
194
|
+
const actions = [];
|
|
195
|
+
if (origWidth > MAX_DIMENSION || origHeight > MAX_DIMENSION) {
|
|
196
|
+
if (origWidth >= origHeight) {
|
|
197
|
+
actions.push({ resize: { width: MAX_DIMENSION } });
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
actions.push({ resize: { height: MAX_DIMENSION } });
|
|
201
|
+
}
|
|
202
|
+
didResize = true;
|
|
203
|
+
}
|
|
204
|
+
const normalized = await ImageManipulator.manipulateAsync(photo.uri, actions, {
|
|
205
|
+
compress: MANIPULATOR_QUALITY,
|
|
206
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
207
|
+
base64: true,
|
|
208
|
+
});
|
|
209
|
+
if (!normalized.base64) {
|
|
210
|
+
throw new Error('ImageManipulator did not return base64');
|
|
211
|
+
}
|
|
212
|
+
base64 = normalized.base64;
|
|
213
|
+
processedWidth = normalized.width;
|
|
214
|
+
processedHeight = normalized.height;
|
|
166
215
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
216
|
+
else {
|
|
217
|
+
// Fallback: capture base64 directly from the camera at reduced quality.
|
|
218
|
+
// No resize is possible without ImageManipulator, but the lower quality
|
|
219
|
+
// significantly reduces payload size (e.g. 50 MP @ 0.5 ≈ 3–4 MB base64).
|
|
220
|
+
const photo = await cameraRef.current.takePictureAsync({
|
|
221
|
+
base64: true,
|
|
222
|
+
quality: FALLBACK_QUALITY,
|
|
223
|
+
exif: false,
|
|
224
|
+
});
|
|
225
|
+
if (!photo?.base64) {
|
|
226
|
+
throw new Error('Failed to capture photo');
|
|
227
|
+
}
|
|
228
|
+
origWidth = photo.width ?? 0;
|
|
229
|
+
origHeight = photo.height ?? 0;
|
|
230
|
+
processedWidth = origWidth;
|
|
231
|
+
processedHeight = origHeight;
|
|
232
|
+
base64 = photo.base64;
|
|
233
|
+
}
|
|
234
|
+
// Best-effort telemetry — never blocks capture
|
|
235
|
+
telemetry?.track('image_processed', {
|
|
236
|
+
component: 'scanner',
|
|
237
|
+
metadata: {
|
|
238
|
+
original_width: origWidth,
|
|
239
|
+
original_height: origHeight,
|
|
240
|
+
processed_width: processedWidth,
|
|
241
|
+
processed_height: processedHeight,
|
|
242
|
+
resized: didResize ? 1 : 0,
|
|
243
|
+
has_manipulator: ImageManipulator ? 1 : 0,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
171
246
|
setStatus('processing');
|
|
172
|
-
const verificationResult = await onCapture(
|
|
247
|
+
const verificationResult = await onCapture(base64);
|
|
173
248
|
attemptCountRef.current++;
|
|
174
249
|
if (verificationResult) {
|
|
175
250
|
const maxAttempts = overlay?.maxAttempts;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
2
2
|
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
3
3
|
export type { UseVerifyAIReturn, UseVerifyAIConfig } from './hooks/useVerifyAI';
|
|
4
|
-
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
5
|
-
export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
|
|
6
4
|
export { TelemetryReporter } from './telemetry/TelemetryReporter';
|
|
7
5
|
export { TelemetryContext } from './telemetry/TelemetryContext';
|
|
8
6
|
export { OfflineQueue } from './storage/offlineQueue';
|
package/lib/index.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
3
3
|
// Hooks
|
|
4
4
|
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
5
|
-
// Components
|
|
6
|
-
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
7
5
|
// Telemetry
|
|
8
6
|
export { TelemetryReporter } from './telemetry/TelemetryReporter';
|
|
9
7
|
export { TelemetryContext } from './telemetry/TelemetryContext';
|
package/lib/scanner.d.ts
ADDED
package/lib/scanner.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
1
|
import { VerifyAIRequestError } from '../client';
|
|
2
|
+
let _storage = null;
|
|
3
|
+
async function getStorage() {
|
|
4
|
+
if (!_storage) {
|
|
5
|
+
const mod = await import('@react-native-async-storage/async-storage');
|
|
6
|
+
_storage = mod.default;
|
|
7
|
+
}
|
|
8
|
+
return _storage;
|
|
9
|
+
}
|
|
3
10
|
const MANIFEST_KEY = '@verifyai/queue_manifest';
|
|
4
11
|
const ITEM_PREFIX = '@verifyai/queue_item_';
|
|
5
12
|
const LEGACY_KEY = '@verifyai/offline_queue';
|
|
@@ -17,13 +24,13 @@ export class OfflineQueue {
|
|
|
17
24
|
if (this.migrated)
|
|
18
25
|
return;
|
|
19
26
|
this.migrated = true;
|
|
20
|
-
const legacy = await
|
|
27
|
+
const legacy = await (await getStorage()).getItem(LEGACY_KEY);
|
|
21
28
|
if (!legacy)
|
|
22
29
|
return;
|
|
23
30
|
try {
|
|
24
31
|
const items = JSON.parse(legacy);
|
|
25
32
|
if (!Array.isArray(items) || items.length === 0) {
|
|
26
|
-
await
|
|
33
|
+
await (await getStorage()).removeItem(LEGACY_KEY);
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
36
|
const ids = [];
|
|
@@ -32,24 +39,25 @@ export class OfflineQueue {
|
|
|
32
39
|
ids.push(item.id);
|
|
33
40
|
pairs.push([`${ITEM_PREFIX}${item.id}`, JSON.stringify(item)]);
|
|
34
41
|
}
|
|
35
|
-
await
|
|
42
|
+
const storage = await getStorage();
|
|
43
|
+
await storage.multiSet([
|
|
36
44
|
[MANIFEST_KEY, JSON.stringify(ids)],
|
|
37
45
|
...pairs,
|
|
38
46
|
]);
|
|
39
|
-
await
|
|
47
|
+
await storage.removeItem(LEGACY_KEY);
|
|
40
48
|
}
|
|
41
49
|
catch {
|
|
42
50
|
// If migration fails, remove corrupt legacy data
|
|
43
|
-
await
|
|
51
|
+
await (await getStorage()).removeItem(LEGACY_KEY);
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
async getManifest() {
|
|
47
55
|
await this.migrateIfNeeded();
|
|
48
|
-
const raw = await
|
|
56
|
+
const raw = await (await getStorage()).getItem(MANIFEST_KEY);
|
|
49
57
|
return raw ? JSON.parse(raw) : [];
|
|
50
58
|
}
|
|
51
59
|
async setManifest(ids) {
|
|
52
|
-
await
|
|
60
|
+
await (await getStorage()).setItem(MANIFEST_KEY, JSON.stringify(ids));
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* Add a verification request to the offline queue.
|
|
@@ -64,7 +72,7 @@ export class OfflineQueue {
|
|
|
64
72
|
};
|
|
65
73
|
const ids = await this.getManifest();
|
|
66
74
|
ids.push(item.id);
|
|
67
|
-
await
|
|
75
|
+
await (await getStorage()).setItem(`${ITEM_PREFIX}${item.id}`, JSON.stringify(item));
|
|
68
76
|
await this.setManifest(ids);
|
|
69
77
|
return item.id;
|
|
70
78
|
}
|
|
@@ -76,7 +84,7 @@ export class OfflineQueue {
|
|
|
76
84
|
if (ids.length === 0)
|
|
77
85
|
return [];
|
|
78
86
|
const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
|
|
79
|
-
const pairs = await
|
|
87
|
+
const pairs = await (await getStorage()).multiGet(keys);
|
|
80
88
|
const items = [];
|
|
81
89
|
for (const [, value] of pairs) {
|
|
82
90
|
if (value) {
|
|
@@ -104,7 +112,7 @@ export class OfflineQueue {
|
|
|
104
112
|
const ids = await this.getManifest();
|
|
105
113
|
const filtered = ids.filter((i) => i !== id);
|
|
106
114
|
await this.setManifest(filtered);
|
|
107
|
-
await
|
|
115
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
108
116
|
}
|
|
109
117
|
/**
|
|
110
118
|
* Clear all items from the queue.
|
|
@@ -113,10 +121,10 @@ export class OfflineQueue {
|
|
|
113
121
|
const ids = await this.getManifest();
|
|
114
122
|
if (ids.length > 0) {
|
|
115
123
|
const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
|
|
116
|
-
await
|
|
124
|
+
await (await getStorage()).multiRemove([MANIFEST_KEY, ...keys]);
|
|
117
125
|
}
|
|
118
126
|
else {
|
|
119
|
-
await
|
|
127
|
+
await (await getStorage()).removeItem(MANIFEST_KEY);
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
/**
|
|
@@ -140,7 +148,7 @@ export class OfflineQueue {
|
|
|
140
148
|
try {
|
|
141
149
|
const ids = await this.getManifest();
|
|
142
150
|
for (const id of ids) {
|
|
143
|
-
const raw = await
|
|
151
|
+
const raw = await (await getStorage()).getItem(`${ITEM_PREFIX}${id}`);
|
|
144
152
|
if (!raw)
|
|
145
153
|
continue;
|
|
146
154
|
let item;
|
|
@@ -149,13 +157,13 @@ export class OfflineQueue {
|
|
|
149
157
|
}
|
|
150
158
|
catch {
|
|
151
159
|
// Remove corrupt item
|
|
152
|
-
await
|
|
160
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
153
161
|
continue;
|
|
154
162
|
}
|
|
155
163
|
try {
|
|
156
164
|
const result = await this.client.verify(item.request, { idempotencyKey: item.id });
|
|
157
165
|
processed++;
|
|
158
|
-
await
|
|
166
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
159
167
|
onResult?.(item.id, result);
|
|
160
168
|
}
|
|
161
169
|
catch (err) {
|
|
@@ -163,12 +171,12 @@ export class OfflineQueue {
|
|
|
163
171
|
const shouldRetry = !requestError || requestError.isRetryable;
|
|
164
172
|
item.retryCount++;
|
|
165
173
|
if (shouldRetry && item.retryCount < maxRetries) {
|
|
166
|
-
await
|
|
174
|
+
await (await getStorage()).setItem(`${ITEM_PREFIX}${id}`, JSON.stringify(item));
|
|
167
175
|
remainingIds.push(id);
|
|
168
176
|
}
|
|
169
177
|
else {
|
|
170
178
|
failed++;
|
|
171
|
-
await
|
|
179
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
}
|
|
@@ -12,6 +12,7 @@ export declare class TelemetryReporter {
|
|
|
12
12
|
component?: string;
|
|
13
13
|
error?: unknown;
|
|
14
14
|
errorCode?: string;
|
|
15
|
+
metadata?: Record<string, string | number>;
|
|
15
16
|
}): void;
|
|
16
17
|
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
17
18
|
flush(): Promise<void>;
|
|
@@ -42,6 +42,7 @@ export class TelemetryReporter {
|
|
|
42
42
|
error_message: errorMessage?.slice(0, 1000),
|
|
43
43
|
error_stack: errorObj?.stack?.slice(0, 2000),
|
|
44
44
|
error_code: opts.errorCode,
|
|
45
|
+
metadata: opts.metadata,
|
|
45
46
|
sdk_platform: Platform.OS,
|
|
46
47
|
sdk_version: SDK_VERSION,
|
|
47
48
|
os_name: Platform.OS,
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "
|
|
1
|
+
export declare const SDK_VERSION = "2.1.0";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '
|
|
1
|
+
export const SDK_VERSION = '2.1.0';
|
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchlabs/verify-ai-react-native",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "React Native SDK for Verify AI - photo verification with AI vision processing",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"default": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./scanner": {
|
|
13
|
+
"types": "./lib/scanner.d.ts",
|
|
14
|
+
"default": "./lib/scanner.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"files": [
|
|
8
18
|
"src",
|
|
9
19
|
"lib"
|
package/src/client/index.ts
CHANGED
|
@@ -10,10 +10,30 @@ import type {
|
|
|
10
10
|
PolicyConfigResponse,
|
|
11
11
|
} from '../types';
|
|
12
12
|
import { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
13
|
+
import { SDK_VERSION } from '../version';
|
|
13
14
|
|
|
14
15
|
const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
|
|
15
16
|
const DEFAULT_TIMEOUT = 30000;
|
|
16
17
|
|
|
18
|
+
const VEHICLE_TYPE_LABELS: Record<string, string> = {
|
|
19
|
+
scooter: 'Scooter',
|
|
20
|
+
'e-bike': 'E-Bike',
|
|
21
|
+
ebike: 'E-Bike',
|
|
22
|
+
bike: 'Bike',
|
|
23
|
+
moped: 'Moped',
|
|
24
|
+
car: 'Car',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Auto-inject sdkVersion and vehicleTypeLabel into metadata. */
|
|
28
|
+
function enrichMetadata(metadata?: Record<string, unknown>): Record<string, unknown> {
|
|
29
|
+
const enriched: Record<string, unknown> = { ...metadata, sdkVersion: SDK_VERSION };
|
|
30
|
+
const vehicleType = enriched.vehicleType;
|
|
31
|
+
if (typeof vehicleType === 'string' && !enriched.vehicleTypeLabel) {
|
|
32
|
+
enriched.vehicleTypeLabel = VEHICLE_TYPE_LABELS[vehicleType.toLowerCase()] || vehicleType;
|
|
33
|
+
}
|
|
34
|
+
return enriched;
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
interface RequestContext {
|
|
18
38
|
path: string;
|
|
19
39
|
url: string;
|
|
@@ -204,10 +224,11 @@ export class VerifyAIClient {
|
|
|
204
224
|
if (options?.idempotencyKey) {
|
|
205
225
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
206
226
|
}
|
|
227
|
+
const enrichedRequest = { ...request, metadata: enrichMetadata(request.metadata) };
|
|
207
228
|
return this.request<VerificationResult>('/verify', {
|
|
208
229
|
method: 'POST',
|
|
209
230
|
headers,
|
|
210
|
-
body: JSON.stringify(
|
|
231
|
+
body: JSON.stringify(enrichedRequest),
|
|
211
232
|
});
|
|
212
233
|
}
|
|
213
234
|
|
|
@@ -215,9 +236,6 @@ export class VerifyAIClient {
|
|
|
215
236
|
* Submit a photo for AI verification using multipart/form-data.
|
|
216
237
|
* Streams the image directly from disk — avoids base64 encoding overhead.
|
|
217
238
|
*
|
|
218
|
-
* **Breaking change:** `onCapture` prop on `VerifyAIScanner` now receives
|
|
219
|
-
* an image URI instead of a base64 string.
|
|
220
|
-
*
|
|
221
239
|
* @param request - Multipart request with file URI and policy
|
|
222
240
|
* @param options - Optional verify options (e.g. idempotency key)
|
|
223
241
|
* @returns The verification result with compliance status and feedback
|
|
@@ -231,9 +249,7 @@ export class VerifyAIClient {
|
|
|
231
249
|
name: 'photo.jpg',
|
|
232
250
|
} as unknown as Blob);
|
|
233
251
|
formData.append('policy', request.policy);
|
|
234
|
-
|
|
235
|
-
formData.append('metadata', JSON.stringify(request.metadata));
|
|
236
|
-
}
|
|
252
|
+
formData.append('metadata', JSON.stringify(enrichMetadata(request.metadata)));
|
|
237
253
|
if (request.provider) {
|
|
238
254
|
formData.append('provider', request.provider);
|
|
239
255
|
}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
CameraView,
|
|
12
12
|
useCameraPermissions,
|
|
13
13
|
} from 'expo-camera';
|
|
14
|
-
import * as ImageManipulator from 'expo-image-manipulator';
|
|
15
14
|
import type {
|
|
16
15
|
VerificationResult,
|
|
17
16
|
ScannerStatus,
|
|
@@ -20,9 +19,16 @@ import type {
|
|
|
20
19
|
import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
21
20
|
import type { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
22
21
|
|
|
22
|
+
/** Quality used when expo-image-manipulator is not available (lower = smaller). */
|
|
23
|
+
const FALLBACK_QUALITY = 0.5;
|
|
24
|
+
/** Quality used when expo-image-manipulator IS available (resize handles size). */
|
|
25
|
+
const MANIPULATOR_QUALITY = 0.7;
|
|
26
|
+
/** Max dimension (px) on longest side when resize is available. */
|
|
27
|
+
const MAX_DIMENSION = 2048;
|
|
28
|
+
|
|
23
29
|
export interface VerifyAIScannerProps {
|
|
24
|
-
/** Called with
|
|
25
|
-
onCapture: (
|
|
30
|
+
/** Called with base64 image data when the user captures a photo. */
|
|
31
|
+
onCapture: (base64: string) => Promise<VerificationResult | null>;
|
|
26
32
|
/** Called when a terminal verification result is reached. */
|
|
27
33
|
onResult?: (result: VerificationResult) => void;
|
|
28
34
|
/** Called when an error occurs. */
|
|
@@ -72,6 +78,8 @@ function getErrorDisplay(error: ErrorWithDetails | null, showTechnicalDetails?:
|
|
|
72
78
|
message = 'Network request failed. Check your connection and try again.';
|
|
73
79
|
} else if (status === 401) {
|
|
74
80
|
message = 'Verification is not configured correctly.';
|
|
81
|
+
} else if (status === 413) {
|
|
82
|
+
message = 'Image is too large. Please try again — the photo will be resized automatically.';
|
|
75
83
|
} else if (status === 429) {
|
|
76
84
|
message = 'Verification is temporarily unavailable. Please try again.';
|
|
77
85
|
} else if (status !== undefined && status >= 500) {
|
|
@@ -101,13 +109,10 @@ function getErrorDisplay(error: ErrorWithDetails | null, showTechnicalDetails?:
|
|
|
101
109
|
*
|
|
102
110
|
* @example
|
|
103
111
|
* ```tsx
|
|
104
|
-
* const {
|
|
105
|
-
* apiKey: 'vai_...',
|
|
106
|
-
* enableOnDeviceML: true,
|
|
107
|
-
* });
|
|
112
|
+
* const { verify } = useVerifyAI({ apiKey: 'vai_...' });
|
|
108
113
|
*
|
|
109
114
|
* <VerifyAIScanner
|
|
110
|
-
* onCapture={(
|
|
115
|
+
* onCapture={(base64) => verify({ image: base64, policy: 'scooter_parking' })}
|
|
111
116
|
* onResult={(result) => {
|
|
112
117
|
* if (result.is_compliant) navigation.navigate('Success');
|
|
113
118
|
* }}
|
|
@@ -230,26 +235,102 @@ export function VerifyAIScanner({
|
|
|
230
235
|
setLastError(null);
|
|
231
236
|
|
|
232
237
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
// --- Capture + best-effort resize ---
|
|
239
|
+
// Strategy: try to dynamically import expo-image-manipulator.
|
|
240
|
+
// If available → resize to MAX_DIMENSION, EXIF-normalize, return base64.
|
|
241
|
+
// If not available → use expo-camera's built-in base64 at lower quality.
|
|
242
|
+
// This keeps expo-image-manipulator as an *optional* dependency.
|
|
243
|
+
let base64: string;
|
|
244
|
+
let origWidth = 0;
|
|
245
|
+
let origHeight = 0;
|
|
246
|
+
let processedWidth = 0;
|
|
247
|
+
let processedHeight = 0;
|
|
248
|
+
let didResize = false;
|
|
249
|
+
|
|
250
|
+
let ImageManipulator: typeof import('expo-image-manipulator') | null = null;
|
|
251
|
+
try {
|
|
252
|
+
ImageManipulator = await import('expo-image-manipulator');
|
|
253
|
+
} catch {
|
|
254
|
+
// Not installed — fall back to camera-only base64 below
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (ImageManipulator) {
|
|
258
|
+
// Capture without base64 — ImageManipulator will produce it after resize.
|
|
259
|
+
const photo = await cameraRef.current.takePictureAsync({
|
|
260
|
+
quality: 0.8,
|
|
261
|
+
exif: false,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (!photo?.uri) {
|
|
265
|
+
throw new Error('Failed to capture photo');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
origWidth = photo.width ?? 0;
|
|
269
|
+
origHeight = photo.height ?? 0;
|
|
270
|
+
|
|
271
|
+
const actions: Array<{ resize: { width?: number; height?: number } }> = [];
|
|
272
|
+
if (origWidth > MAX_DIMENSION || origHeight > MAX_DIMENSION) {
|
|
273
|
+
if (origWidth >= origHeight) {
|
|
274
|
+
actions.push({ resize: { width: MAX_DIMENSION } });
|
|
275
|
+
} else {
|
|
276
|
+
actions.push({ resize: { height: MAX_DIMENSION } });
|
|
277
|
+
}
|
|
278
|
+
didResize = true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const normalized = await ImageManipulator.manipulateAsync(
|
|
282
|
+
photo.uri,
|
|
283
|
+
actions,
|
|
284
|
+
{
|
|
285
|
+
compress: MANIPULATOR_QUALITY,
|
|
286
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
287
|
+
base64: true,
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!normalized.base64) {
|
|
292
|
+
throw new Error('ImageManipulator did not return base64');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
base64 = normalized.base64;
|
|
296
|
+
processedWidth = normalized.width;
|
|
297
|
+
processedHeight = normalized.height;
|
|
298
|
+
} else {
|
|
299
|
+
// Fallback: capture base64 directly from the camera at reduced quality.
|
|
300
|
+
// No resize is possible without ImageManipulator, but the lower quality
|
|
301
|
+
// significantly reduces payload size (e.g. 50 MP @ 0.5 ≈ 3–4 MB base64).
|
|
302
|
+
const photo = await cameraRef.current.takePictureAsync({
|
|
303
|
+
base64: true,
|
|
304
|
+
quality: FALLBACK_QUALITY,
|
|
305
|
+
exif: false,
|
|
306
|
+
});
|
|
237
307
|
|
|
238
|
-
|
|
239
|
-
|
|
308
|
+
if (!photo?.base64) {
|
|
309
|
+
throw new Error('Failed to capture photo');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
origWidth = photo.width ?? 0;
|
|
313
|
+
origHeight = photo.height ?? 0;
|
|
314
|
+
processedWidth = origWidth;
|
|
315
|
+
processedHeight = origHeight;
|
|
316
|
+
base64 = photo.base64;
|
|
240
317
|
}
|
|
241
318
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
// Best-effort telemetry — never blocks capture
|
|
320
|
+
telemetry?.track('image_processed', {
|
|
321
|
+
component: 'scanner',
|
|
322
|
+
metadata: {
|
|
323
|
+
original_width: origWidth,
|
|
324
|
+
original_height: origHeight,
|
|
325
|
+
processed_width: processedWidth,
|
|
326
|
+
processed_height: processedHeight,
|
|
327
|
+
resized: didResize ? 1 : 0,
|
|
328
|
+
has_manipulator: ImageManipulator ? 1 : 0,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
250
331
|
|
|
251
332
|
setStatus('processing');
|
|
252
|
-
const verificationResult = await onCapture(
|
|
333
|
+
const verificationResult = await onCapture(base64);
|
|
253
334
|
|
|
254
335
|
attemptCountRef.current++;
|
|
255
336
|
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,6 @@ export { VerifyAIClient, VerifyAIRequestError } from './client';
|
|
|
5
5
|
export { useVerifyAI } from './hooks/useVerifyAI';
|
|
6
6
|
export type { UseVerifyAIReturn, UseVerifyAIConfig } from './hooks/useVerifyAI';
|
|
7
7
|
|
|
8
|
-
// Components
|
|
9
|
-
export { VerifyAIScanner } from './components/VerifyAIScanner';
|
|
10
|
-
export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
|
|
11
|
-
|
|
12
8
|
// Telemetry
|
|
13
9
|
export { TelemetryReporter } from './telemetry/TelemetryReporter';
|
|
14
10
|
export { TelemetryContext } from './telemetry/TelemetryContext';
|
package/src/scanner.ts
ADDED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
1
|
import type { VerificationRequest, VerificationResult, QueueItem } from '../types';
|
|
3
2
|
import { VerifyAIClient, VerifyAIRequestError } from '../client';
|
|
4
3
|
|
|
4
|
+
let _storage: typeof import('@react-native-async-storage/async-storage').default | null = null;
|
|
5
|
+
async function getStorage() {
|
|
6
|
+
if (!_storage) {
|
|
7
|
+
const mod = await import('@react-native-async-storage/async-storage');
|
|
8
|
+
_storage = mod.default;
|
|
9
|
+
}
|
|
10
|
+
return _storage;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
const MANIFEST_KEY = '@verifyai/queue_manifest';
|
|
6
14
|
const ITEM_PREFIX = '@verifyai/queue_item_';
|
|
7
15
|
const LEGACY_KEY = '@verifyai/offline_queue';
|
|
@@ -23,13 +31,13 @@ export class OfflineQueue {
|
|
|
23
31
|
if (this.migrated) return;
|
|
24
32
|
this.migrated = true;
|
|
25
33
|
|
|
26
|
-
const legacy = await
|
|
34
|
+
const legacy = await (await getStorage()).getItem(LEGACY_KEY);
|
|
27
35
|
if (!legacy) return;
|
|
28
36
|
|
|
29
37
|
try {
|
|
30
38
|
const items: QueueItem[] = JSON.parse(legacy);
|
|
31
39
|
if (!Array.isArray(items) || items.length === 0) {
|
|
32
|
-
await
|
|
40
|
+
await (await getStorage()).removeItem(LEGACY_KEY);
|
|
33
41
|
return;
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -40,25 +48,26 @@ export class OfflineQueue {
|
|
|
40
48
|
pairs.push([`${ITEM_PREFIX}${item.id}`, JSON.stringify(item)]);
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
await
|
|
51
|
+
const storage = await getStorage();
|
|
52
|
+
await storage.multiSet([
|
|
44
53
|
[MANIFEST_KEY, JSON.stringify(ids)],
|
|
45
54
|
...pairs,
|
|
46
55
|
]);
|
|
47
|
-
await
|
|
56
|
+
await storage.removeItem(LEGACY_KEY);
|
|
48
57
|
} catch {
|
|
49
58
|
// If migration fails, remove corrupt legacy data
|
|
50
|
-
await
|
|
59
|
+
await (await getStorage()).removeItem(LEGACY_KEY);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
private async getManifest(): Promise<string[]> {
|
|
55
64
|
await this.migrateIfNeeded();
|
|
56
|
-
const raw = await
|
|
65
|
+
const raw = await (await getStorage()).getItem(MANIFEST_KEY);
|
|
57
66
|
return raw ? JSON.parse(raw) : [];
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
private async setManifest(ids: string[]): Promise<void> {
|
|
61
|
-
await
|
|
70
|
+
await (await getStorage()).setItem(MANIFEST_KEY, JSON.stringify(ids));
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
/**
|
|
@@ -76,7 +85,7 @@ export class OfflineQueue {
|
|
|
76
85
|
const ids = await this.getManifest();
|
|
77
86
|
ids.push(item.id);
|
|
78
87
|
|
|
79
|
-
await
|
|
88
|
+
await (await getStorage()).setItem(`${ITEM_PREFIX}${item.id}`, JSON.stringify(item));
|
|
80
89
|
await this.setManifest(ids);
|
|
81
90
|
return item.id;
|
|
82
91
|
}
|
|
@@ -89,7 +98,7 @@ export class OfflineQueue {
|
|
|
89
98
|
if (ids.length === 0) return [];
|
|
90
99
|
|
|
91
100
|
const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
|
|
92
|
-
const pairs = await
|
|
101
|
+
const pairs = await (await getStorage()).multiGet(keys);
|
|
93
102
|
|
|
94
103
|
const items: QueueItem[] = [];
|
|
95
104
|
for (const [, value] of pairs) {
|
|
@@ -119,7 +128,7 @@ export class OfflineQueue {
|
|
|
119
128
|
const ids = await this.getManifest();
|
|
120
129
|
const filtered = ids.filter((i) => i !== id);
|
|
121
130
|
await this.setManifest(filtered);
|
|
122
|
-
await
|
|
131
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/**
|
|
@@ -129,9 +138,9 @@ export class OfflineQueue {
|
|
|
129
138
|
const ids = await this.getManifest();
|
|
130
139
|
if (ids.length > 0) {
|
|
131
140
|
const keys = ids.map((id) => `${ITEM_PREFIX}${id}`);
|
|
132
|
-
await
|
|
141
|
+
await (await getStorage()).multiRemove([MANIFEST_KEY, ...keys]);
|
|
133
142
|
} else {
|
|
134
|
-
await
|
|
143
|
+
await (await getStorage()).removeItem(MANIFEST_KEY);
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
146
|
|
|
@@ -162,7 +171,7 @@ export class OfflineQueue {
|
|
|
162
171
|
const ids = await this.getManifest();
|
|
163
172
|
|
|
164
173
|
for (const id of ids) {
|
|
165
|
-
const raw = await
|
|
174
|
+
const raw = await (await getStorage()).getItem(`${ITEM_PREFIX}${id}`);
|
|
166
175
|
if (!raw) continue;
|
|
167
176
|
|
|
168
177
|
let item: QueueItem;
|
|
@@ -170,14 +179,14 @@ export class OfflineQueue {
|
|
|
170
179
|
item = JSON.parse(raw);
|
|
171
180
|
} catch {
|
|
172
181
|
// Remove corrupt item
|
|
173
|
-
await
|
|
182
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
174
183
|
continue;
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
try {
|
|
178
187
|
const result = await this.client.verify(item.request, { idempotencyKey: item.id });
|
|
179
188
|
processed++;
|
|
180
|
-
await
|
|
189
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
181
190
|
onResult?.(item.id, result);
|
|
182
191
|
} catch (err) {
|
|
183
192
|
const requestError = err instanceof VerifyAIRequestError ? err : null;
|
|
@@ -185,11 +194,11 @@ export class OfflineQueue {
|
|
|
185
194
|
|
|
186
195
|
item.retryCount++;
|
|
187
196
|
if (shouldRetry && item.retryCount < maxRetries) {
|
|
188
|
-
await
|
|
197
|
+
await (await getStorage()).setItem(`${ITEM_PREFIX}${id}`, JSON.stringify(item));
|
|
189
198
|
remainingIds.push(id);
|
|
190
199
|
} else {
|
|
191
200
|
failed++;
|
|
192
|
-
await
|
|
201
|
+
await (await getStorage()).removeItem(`${ITEM_PREFIX}${id}`);
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
}
|
|
@@ -7,6 +7,7 @@ interface TelemetryEvent {
|
|
|
7
7
|
error_message?: string;
|
|
8
8
|
error_stack?: string;
|
|
9
9
|
error_code?: string;
|
|
10
|
+
metadata?: Record<string, string | number>;
|
|
10
11
|
sdk_platform: string;
|
|
11
12
|
sdk_version: string;
|
|
12
13
|
os_name: string;
|
|
@@ -51,6 +52,7 @@ export class TelemetryReporter {
|
|
|
51
52
|
component?: string;
|
|
52
53
|
error?: unknown;
|
|
53
54
|
errorCode?: string;
|
|
55
|
+
metadata?: Record<string, string | number>;
|
|
54
56
|
} = {},
|
|
55
57
|
): void {
|
|
56
58
|
if (this.disposed) return;
|
|
@@ -80,6 +82,7 @@ export class TelemetryReporter {
|
|
|
80
82
|
error_message: errorMessage?.slice(0, 1000),
|
|
81
83
|
error_stack: errorObj?.stack?.slice(0, 2000),
|
|
82
84
|
error_code: opts.errorCode,
|
|
85
|
+
metadata: opts.metadata,
|
|
83
86
|
sdk_platform: Platform.OS,
|
|
84
87
|
sdk_version: SDK_VERSION,
|
|
85
88
|
os_name: Platform.OS,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '
|
|
1
|
+
export const SDK_VERSION = '2.1.0';
|