@switchlabs/verify-ai-react-native 2.3.1 → 2.4.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/lib/client/index.js +5 -1
- package/lib/components/BikeOverlay.d.ts +12 -0
- package/lib/components/BikeOverlay.js +17 -0
- package/lib/components/ScooterOverlay.d.ts +12 -0
- package/lib/components/ScooterOverlay.js +17 -0
- package/lib/components/VerifyAIScanner.js +30 -16
- package/lib/components/overlayAssets.d.ts +4 -0
- package/lib/components/overlayAssets.js +6 -0
- package/lib/index.d.ts +1 -1
- package/lib/scanner.d.ts +2 -0
- package/lib/scanner.js +2 -0
- package/lib/telemetry/TelemetryReporter.d.ts +16 -0
- package/lib/telemetry/TelemetryReporter.js +135 -0
- package/lib/types/index.d.ts +25 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -8
- package/src/client/index.ts +5 -1
- package/src/components/BikeOverlay.tsx +24 -0
- package/src/components/ScooterOverlay.tsx +24 -0
- package/src/components/VerifyAIScanner.tsx +36 -21
- package/src/components/overlayAssets.ts +8 -0
- package/src/index.ts +1 -0
- package/src/scanner.ts +2 -0
- package/src/telemetry/TelemetryReporter.ts +138 -0
- package/src/types/index.ts +26 -0
- package/src/version.ts +1 -1
package/lib/client/index.js
CHANGED
|
@@ -188,6 +188,9 @@ export class VerifyAIClient {
|
|
|
188
188
|
if (request.provider) {
|
|
189
189
|
formData.append('provider', request.provider);
|
|
190
190
|
}
|
|
191
|
+
if (request.include_image_data) {
|
|
192
|
+
formData.append('include_image_data', 'true');
|
|
193
|
+
}
|
|
191
194
|
const headers = {
|
|
192
195
|
'X-API-Key': this.apiKey,
|
|
193
196
|
// Do NOT set Content-Type — fetch auto-sets multipart boundary
|
|
@@ -217,6 +220,7 @@ export class VerifyAIClient {
|
|
|
217
220
|
policy: request.policy,
|
|
218
221
|
metadata: request.metadata,
|
|
219
222
|
provider: request.provider,
|
|
223
|
+
include_image_data: request.include_image_data,
|
|
220
224
|
});
|
|
221
225
|
}
|
|
222
226
|
catch {
|
|
@@ -313,7 +317,7 @@ export class VerifyAIRequestError extends Error {
|
|
|
313
317
|
return this.status >= 500;
|
|
314
318
|
}
|
|
315
319
|
get isRetryable() {
|
|
316
|
-
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
320
|
+
return this.status === 0 || this.status === 408 || this.status === 429 || this.status >= 500;
|
|
317
321
|
}
|
|
318
322
|
get upgradeUrl() {
|
|
319
323
|
return this.body.upgrade_url;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in bike silhouette overlay for the scanner guide frame.
|
|
3
|
+
*
|
|
4
|
+
* Renders a white bike silhouette from an embedded PNG asset.
|
|
5
|
+
* Pass as `guideOverlayContent` in `ScannerOverlayConfig`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* overlay={{ guideOverlayContent: <BikeOverlay />, guideOverlayOpacity: 0.3 }}
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function BikeOverlay(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Image } from 'react-native';
|
|
3
|
+
import { BIKE_PNG_BASE64 } from './overlayAssets';
|
|
4
|
+
/**
|
|
5
|
+
* Built-in bike silhouette overlay for the scanner guide frame.
|
|
6
|
+
*
|
|
7
|
+
* Renders a white bike silhouette from an embedded PNG asset.
|
|
8
|
+
* Pass as `guideOverlayContent` in `ScannerOverlayConfig`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* overlay={{ guideOverlayContent: <BikeOverlay />, guideOverlayOpacity: 0.3 }}
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function BikeOverlay() {
|
|
16
|
+
return (_jsx(Image, { source: { uri: `data:image/png;base64,${BIKE_PNG_BASE64}` }, style: { width: '100%', height: '100%' }, resizeMode: "contain" }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in scooter silhouette overlay for the scanner guide frame.
|
|
3
|
+
*
|
|
4
|
+
* Renders a white scooter silhouette from an embedded PNG asset.
|
|
5
|
+
* Pass as `guideOverlayContent` in `ScannerOverlayConfig`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* overlay={{ guideOverlayContent: <ScooterOverlay />, guideOverlayOpacity: 0.3 }}
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function ScooterOverlay(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Image } from 'react-native';
|
|
3
|
+
import { SCOOTER_PNG_BASE64 } from './overlayAssets';
|
|
4
|
+
/**
|
|
5
|
+
* Built-in scooter silhouette overlay for the scanner guide frame.
|
|
6
|
+
*
|
|
7
|
+
* Renders a white scooter silhouette from an embedded PNG asset.
|
|
8
|
+
* Pass as `guideOverlayContent` in `ScannerOverlayConfig`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* overlay={{ guideOverlayContent: <ScooterOverlay />, guideOverlayOpacity: 0.3 }}
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function ScooterOverlay() {
|
|
16
|
+
return (_jsx(Image, { source: { uri: `data:image/png;base64,${SCOOTER_PNG_BASE64}` }, style: { width: '100%', height: '100%' }, resizeMode: "contain" }));
|
|
17
|
+
}
|
|
@@ -4,11 +4,11 @@ import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, } from 're
|
|
|
4
4
|
import { CameraView, useCameraPermissions, } from 'expo-camera';
|
|
5
5
|
import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
6
6
|
/** Quality used when expo-image-manipulator is not available (lower = smaller). */
|
|
7
|
-
const FALLBACK_QUALITY = 0.
|
|
7
|
+
const FALLBACK_QUALITY = 0.65;
|
|
8
8
|
/** Quality used when expo-image-manipulator IS available (resize handles size). */
|
|
9
|
-
const MANIPULATOR_QUALITY = 0.
|
|
9
|
+
const MANIPULATOR_QUALITY = 0.8;
|
|
10
10
|
/** Max dimension (px) on longest side when resize is available. */
|
|
11
|
-
const MAX_DIMENSION =
|
|
11
|
+
const MAX_DIMENSION = 1600;
|
|
12
12
|
function getErrorDisplay(error, showTechnicalDetails) {
|
|
13
13
|
if (!error) {
|
|
14
14
|
return {
|
|
@@ -85,19 +85,21 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
85
85
|
const [exhausted, setExhausted] = useState(false);
|
|
86
86
|
const [terminated, setTerminated] = useState(false);
|
|
87
87
|
const [cameraReady, setCameraReady] = useState(false);
|
|
88
|
+
const [cameraKey, setCameraKey] = useState(0);
|
|
88
89
|
const cameraReadyRef = useRef(false);
|
|
89
90
|
const cameraInitFailedRef = useRef(false);
|
|
90
91
|
const permissionDeniedTrackedRef = useRef(false);
|
|
92
|
+
const pausePreview = useCallback(() => {
|
|
93
|
+
cameraRef.current?.pausePreview?.().catch(() => { });
|
|
94
|
+
}, []);
|
|
91
95
|
// Release camera (and torch) when a terminal result is reached or on unmount.
|
|
92
96
|
const releaseCamera = useCallback(() => {
|
|
93
97
|
setTerminated(true);
|
|
94
|
-
|
|
95
|
-
}, []);
|
|
98
|
+
pausePreview();
|
|
99
|
+
}, [pausePreview]);
|
|
96
100
|
useEffect(() => {
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
};
|
|
100
|
-
}, []);
|
|
101
|
+
return pausePreview;
|
|
102
|
+
}, [pausePreview]);
|
|
101
103
|
// Camera init callbacks
|
|
102
104
|
const onCameraReady = useCallback(() => {
|
|
103
105
|
setCameraReady(true);
|
|
@@ -118,7 +120,10 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
118
120
|
error,
|
|
119
121
|
});
|
|
120
122
|
}, [onError, telemetry]);
|
|
121
|
-
// Startup watchdog — if camera hasn't fired onCameraReady within
|
|
123
|
+
// Startup watchdog — if camera hasn't fired onCameraReady within 3s, force remount.
|
|
124
|
+
// On iOS the native AVCaptureSession sometimes fails to start its preview layer on first
|
|
125
|
+
// mount (e.g. during navigation transitions). Changing the key forces React to destroy and
|
|
126
|
+
// recreate the CameraView, which starts a fresh native session.
|
|
122
127
|
useEffect(() => {
|
|
123
128
|
if (!permission?.granted || terminated)
|
|
124
129
|
return;
|
|
@@ -126,12 +131,16 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
126
131
|
if (!cameraReadyRef.current && !cameraInitFailedRef.current) {
|
|
127
132
|
telemetry?.track('camera_preview_timeout', {
|
|
128
133
|
component: 'scanner',
|
|
129
|
-
error: 'Camera did not initialize within
|
|
134
|
+
error: 'Camera did not initialize within 3 seconds — remounting',
|
|
130
135
|
});
|
|
136
|
+
// Reset state and bump key to force a fresh CameraView mount
|
|
137
|
+
setCameraReady(false);
|
|
138
|
+
cameraReadyRef.current = false;
|
|
139
|
+
setCameraKey((k) => k + 1);
|
|
131
140
|
}
|
|
132
|
-
},
|
|
141
|
+
}, 3000);
|
|
133
142
|
return () => clearTimeout(timer);
|
|
134
|
-
}, [permission?.granted, terminated, telemetry]);
|
|
143
|
+
}, [permission?.granted, terminated, cameraKey, telemetry]);
|
|
135
144
|
// Track permission denied
|
|
136
145
|
useEffect(() => {
|
|
137
146
|
if (permission &&
|
|
@@ -317,12 +326,16 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
317
326
|
overlay.guideFrameAspectRatio
|
|
318
327
|
? { aspectRatio: overlay.guideFrameAspectRatio }
|
|
319
328
|
: undefined,
|
|
320
|
-
], children: [overlay.guideOverlayContent && (_jsx(View, { style: [StyleSheet.absoluteFill, { opacity: overlay.guideOverlayOpacity ?? 0.3 }], children: overlay.guideOverlayContent })), _jsx(View, { style: [styles.corner, styles.cornerTopLeft] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight] })] }), overlay.guideCaption && (_jsx(Text, { style: styles.guideCaptionText, children: overlay.guideCaption }))] })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: overlay?.processingMessage || 'Analyzing photo...' })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: styles.bottomArea, children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
|
|
329
|
+
], children: [overlay.guideOverlayContent && (_jsx(View, { style: [StyleSheet.absoluteFill, { opacity: overlay.guideOverlayOpacity ?? 0.3 }], children: overlay.guideOverlayContent })), _jsx(View, { style: [styles.corner, styles.cornerTopLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] })] }), overlay.guideCaption && (_jsx(Text, { style: styles.guideCaptionText, children: overlay.guideCaption }))] })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: overlay?.processingMessage || 'Analyzing photo...' })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: styles.bottomArea, children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
|
|
321
330
|
styles.resultIconCircle,
|
|
322
331
|
result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
|
|
332
|
+
result.is_compliant && overlay?.theme?.successColor ? { backgroundColor: overlay.theme.successColor } : undefined,
|
|
333
|
+
!result.is_compliant && overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined,
|
|
323
334
|
], children: _jsx(Text, { style: styles.resultIcon, children: result.is_compliant ? '\u2713' : '\u2717' }) }), _jsx(Text, { style: [
|
|
324
335
|
styles.resultLabel,
|
|
325
336
|
result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
|
|
337
|
+
result.is_compliant && overlay?.theme?.successColor ? { color: overlay.theme.successColor } : undefined,
|
|
338
|
+
!result.is_compliant && overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined,
|
|
326
339
|
], children: result.is_compliant
|
|
327
340
|
? (overlay?.successMessage || 'Verified')
|
|
328
341
|
: (overlay?.failureMessage || 'Not Verified') })] }), _jsx(Text, { style: styles.feedbackText, children: result.feedback })] })), status === 'error' && (() => {
|
|
@@ -350,12 +363,13 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
|
|
|
350
363
|
errorTitle = display.title;
|
|
351
364
|
errorMessage = display.message;
|
|
352
365
|
}
|
|
353
|
-
return (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [styles.resultIconCircle, styles.resultIconError], children: _jsx(Text, { style: styles.resultIcon, children: "!" }) }), _jsx(Text, { style: [styles.resultLabel, styles.resultLabelError], children: errorTitle })] }), _jsx(Text, { style: styles.feedbackText, children: errorMessage })] }));
|
|
366
|
+
return (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [styles.resultIconCircle, styles.resultIconError, overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined], children: _jsx(Text, { style: styles.resultIcon, children: "!" }) }), _jsx(Text, { style: [styles.resultLabel, styles.resultLabelError, overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined], children: errorTitle })] }), _jsx(Text, { style: styles.feedbackText, children: errorMessage })] }));
|
|
354
367
|
})(), !showBottomCard && (_jsxs(_Fragment, { children: [overlay?.instructions && status === 'idle' && (_jsx(Text, { style: styles.instructionsText, children: overlay.instructions })), showCaptureButton && (_jsx(View, { style: styles.captureButtonRow, children: _jsx(TouchableOpacity, { style: [
|
|
355
368
|
styles.captureButton,
|
|
369
|
+
overlay?.theme?.captureButtonColor ? { borderColor: overlay.theme.captureButtonColor } : undefined,
|
|
356
370
|
(!cameraReady || status === 'capturing' || status === 'processing') &&
|
|
357
371
|
styles.captureButtonDisabled,
|
|
358
|
-
], onPress: handleCapture, disabled: !cameraReady || status === 'capturing' || status === 'processing', activeOpacity: 0.7, children: _jsx(View, { style: styles.captureButtonInner }) }) }))] }))] })] }) }) }));
|
|
372
|
+
], onPress: handleCapture, disabled: !cameraReady || status === 'capturing' || status === 'processing', activeOpacity: 0.7, children: _jsx(View, { style: [styles.captureButtonInner, overlay?.theme?.captureButtonColor ? { backgroundColor: overlay.theme.captureButtonColor } : undefined] }) }) }))] }))] })] }) }, cameraKey) }));
|
|
359
373
|
}
|
|
360
374
|
const CORNER_SIZE = 30;
|
|
361
375
|
const CORNER_THICKNESS = 3;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Base64-encoded scooter silhouette PNG. */
|
|
2
|
+
export declare const SCOOTER_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAyAAAAMgCAYAAADbcAZoAAAWkUlEQVR4nO3d63LbxhKFUSvl939lnfJJuXyJRPGC2dPTvdbvxCZAoIcfQIPfvgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN/edr8AAPp7f39/f+X/f3t7s15xCcci7OckAqDsh72/+fDHsxyLUMc/u18AAD1d/YFv1Z9Jf45FqEWAAPDtpA9nPvjxCMci1CNAADjuQ5kPftzDsQg1CRAAACDGP6AC4DKuBjOVf5QO93MHBAAAiBEgAABAjAABAABiBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAAMQIEAACIESAAAEDMW+6vAmCC9/f398Tf8/b2Zg1b+B502L+ORajJHRAAaOaKD8SpD+/APAIEgOOuBrvizD0ci1CTAAHgqA9lPvDxCMci1CNAADjmw5kPfDzDsQi1OHkAWO7Vf0/gw97j/EP0jzkWYT8nEQA0JECAqnwFCwAAiBEgAABAjAABgIb8FghQlQABAABiBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAmvIoXqAiAQIAAMQIEAAAIEaAAAAAMQIEAACIESAAAECMAAEAAGIECAA05lG8QDUCBAAAiBEgAABAjAABAABiBAgAABAjQAAAgBgBAgAAxAgQAGjuikfxAlxFgAAAX/JbIMBVBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAwwBW/BeJRvMAVBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAwhEfxAhUIEAAAIEaAAAAAMQIEAACIESAAAECMAAEAAGIECAAAECNAAGAQj+IFdhMgAABAjAABAABiBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAhvFbIMBOAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAYyKN4gV0ECAAAECNAAACAGAECAADECBAAACBGgAAAADECBAAAiBEgADCUR/ECOwgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAYDCP4gXSBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAYDh/BYIkCRAAACAGAECAADECBAAACBGgAAAADECBAAAiBEgAABAjAABADyKF4gRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAD/51G8QIIAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAAMQIEALj0UbwAtwgQAOBSfgsEuEWAAAAAMQIEAACIESAAAECMAAEAAGIECAAAECNAAACAGAECAFz+WyAexQt8RoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIA/IdH8QKrCBAAACBGgAAAADECBAAAiBEgAABAjAABAABiBAgAABAjQACAD3kUL7CCAAEAAGIECAAAECNAAACAGAECAADECBAAACBGgAAAADECBAAAiBEgAMCn/BYIcDUBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAHCTR/ECVxIgAABAjAABAABiBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAF/yKF7gKgIEAACIESAAAECMAAEAAGIECAAAECNAAACAGAECAADECBAA4C4exQtcQYAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEADgbn4LBHiVAAEAAGIECAAAECNAAACAGAECAADECBAAACBGgAAAADECBAB4iEfxAq8QIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAP8yhe4FkCBAAAiBEgAABAjAABAABiBAgAABAjQAAAgBgBAgAAxAgQAGDbo3iBeQQIALCN3wKBeQQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAAMQIEAACIESAAwNbfAvEoXphFgAAAADECBAAAiBEgAABAjAABAABiBAgAABAjQAAAgBgBAgC8xKN4gUcIEAAAIEaAAAAAMQIEAACIESAAAECMAAEAAGIECAAAECNAAICXeRQvcC8BAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAgEv4LRDgHgIEAACIESAAAECMAAEAAGIECAAAECNAAACAGAECAADECBAA4DIexQt8RYAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAXMqjeIFbBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIADA5TyKF/iMAAEAAGIECAAAECNAAACAGAECAADECBAAACBGgAAAADECBAAAiBEgAMASfgsE+IgAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAAMQIEAFjGo3iBvwkQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAgKU8ihf4nQABAABiBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAOOJRvEAPAgQAOILfAoEeBAgAABAjQAAAgBgBAgAAxAgQAAAgRoAAAAAxAgQAAIgRIADAMb8F4lG8cD4BAgAAxAgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIABDjUbyAAAEAAGIECAAAECNAAACAGAECAADECBAAACBGgAAAADECBACI8ihemE2AAAAAMQIEAACIESAAAECMAAEAAGIECAAAECNAAACAGAECAADECBAAIM5vgcBcAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEABgC4/ihZkECAAAECNAAACAGAECAADECBAAACBGgAAAADECBAAAiBEgAMA2HsUL8wgQAAAgRoAAAAAxAgQAAIgRIAAAQIwAAQAAYgQIAAAQI0AAgK08ihdmESAAAECMAAEAAGIECAAAECNAAACAGAECAADECBAAACBGgAAAADECBADYzm+BwBwCBAAAiBEgAABAjAABAABiBAgAABAjQAAAgBgBAgAAxAgQAKAEj+KFGQQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAAMQIEAACIESAAQBkexQv9CRAAACBGgAAAADECBAAAiBEgAABAjAABAABiBAgAABAjQACAdo/iBeoSIABAO34LBOoSIAAAQIwAAQAAYgQIAAAQI0AAAIAYAQIAAMQIEAAAIEaAAAAtfwvEo3ihJgECAADECBAAACBGgAAAADECBAAAiBEgAABAjAABAABiBAgAUJJH8UJP33e/AOjiykXuikUXAKAiAQIFr6Z99ncIEwDgdAIEDrp1/9HrESUAwEkECBSPjkderxgBAKoTIIx3WnA8si2CBACoRoAwVqfw+GobhQgAUIUPJYwyITq+IkaAibPb7IM63AFhBOHxi7siAMBOAoTWhMfnhAgAsIMAoSXhcT8hAgAkCRBaER7PEyIAQIIAoQXhcR0hAgCsJEA4WrXwuOJDe5VtEiIAwAo+WHCsnR/Ud3won7a9AD95DC/04mTkODs+iN+7cCUXycr7AeBqFe4Om4FwDScSR0ktQF8tMhU//FfZNwBdA+Qz5iI8xgnDMVYvPrcWkIoL387Xa7EF0irO4c+YkXCbE4Tydn2Y7rDYCRGgi5Nm8u/MSfgvJwUjF5wO0VFl2yyuQELn+QzTOBEYtdh8NPw7LGq7t9eiCqwwbT7DFA5+Riw608Jjx/ZbTIGrTJ/P0J2DnlKEx1pCBKjMfIYZHOyUsfqD8OSFLb1vLKTAI8znX8xPJnCQ027xER419pVFFPiK+fw5M5TOHNxsJz4A4L9ECF05sNnqqkAQHgB0JUTo5p/dL4C5xAcAfM26RjeKmjbxYUAD0J27IXTgICbuilBw1wOAqUQIp/MVLI4jPgCYzLrH6RQ0Rw1NX7kCgF/cDeFE7oAQIz4A4FrWQ04kQIgQHwCwhnWR0wgQlhMfALCW9ZGTCBBKEx8AcB/rJKcQIJQdhuIDAB5jveQEAoRlxAcA5Fk3qU6AsIT4AIB9rJ9UJkAoRXwAwDWso1QlQCg58AxNAHid9ZSKBAglv3oFAEBPAoQSfPUKANawrlKNAGH7gBMfALCW9ZVKBAhlGI4AsI51lioECGXufgAA0J8AYRtfvQKALOstFQgQtg+zjsPwR1y5uwNwtq6zvOO6y1m+734BzNRtoH+2Pa9sZ4cF4tW7XN2OEzhNtzl01Z/RYb/AThZ3XvLqh8qTh3jyw3GX/SRC4Bxd5s5q9hM8zlew2ObUob3jlvzJXwM49X2GyU49b83nGe8z5xMgPG3a1ewKi0yF1/CKZ167BRI4YTZWeA1wCgHCFid9qKy4qFR8TV3eb5jupPO14iys+Jq6vN/0IUB4yoS7HycsIie8xr+5CwJMmH0nvEbYRYAQd8KHydMWjRNe7wnvO0x3wnl6wrw77fWe8L7TiwAh4oQBfOJrPfV1n/RagTpOnR2nvm5YxQlB9OtXla+ydFogTtjPE77GByc5YW50YD+DOyA8oeP3Wm1PbR2POeA+3c79btsDz3ASsPyKTvW7H50Xg+r7/J7jBliv+qzoyD5nMndAGH2F+sTX3Hn7fh5Hp71u4Hrd50D37YNbHPyMvdozafjb98AtZsQ+9j0TuQPCyAHLXo4HqMP5yO8cDyQIEEaadoVn2vYC55o2r6ZtL/wgQBhn6rCfut3AOabOqanbzVwChFG3c6cP+UrbX+m4gKkqnYeV5tP07a90XNCTAAEAAGIECGNUurq0k/0AVGMu/ct+YAoBwlJu43KL4wP2cf5xi+ODlQQII7iq9Cf7A6jCPPqT/cEEAgQAAIgRILTnatLH7BdgN3PoY/YL3QkQlvH9Ue7hOIE85x33cJywigChNVeRbrN/gF3Mn9vsHzoTIAAAQIwAAQAAYr7n/iqYefv61ndoK7zGH6/B93yBpAqz7wfzGfYQILDIPYvGz/+mwkIHMIX5DHv5ChZLTL9i8+j221+ztx+Spp9v5vNjpm8/awgQKDKsDXmAtcxnqEGA0NKuW+avLlK7FjlfMQBSzOfHmM90JEDgIlctTq60AVzLfIZaBAgAABAjQKDgVTFX2QCuYT5DPQIEAACIESAAAECMAAEAAGIECAAAECNAAACAGAECAADECBAo+Eu1fvkW4BrmM9QjQAAAgBgBAsWuirm6BnAt8xlqESC0tOuXal9dnHYtbn7ZF0gxnx9jPtORAIGLPbtIubIGsJb5DDUIEJaYPqwf3X77a/b2Q9L08818fsz07WeN74v+XBjv59C+dfvcYAfIM59hLycXrb+3agH5mvcJ5nHen8H7RFe+ggUAAMQIEAAAIEaA0FqF29eV2T/ALubPbfYPnQkQlvG9Ue7hOIE85x33cJywigChPVeRPma/ALuZQx+zX+hOgAAAADEChBFcTfqT/QFUYR79yf5gAgHCUr4/yi2OD9jH+cctjg9WEiCM4arSv+wHoBpz6V/2A1MIEAAAIEaAMOo27vSrS5W2v9JxAVNVOg8rzafp21/puKAnAcI4lYZ80tTtBs4xdU5N3W7mEiCMNG3YT9te4FzT5tW07YUfBAgRbufyO8cD1OF85HeOBxIECGNNueo0ZTuBPqbMrSnbCX8TIIzWffh33z6gr+7zq/v2wS0ChG/Tb+t2XQSqblfV4wAmq3peVp1jXber6nFAPwIECi8Gz+q2PcBc3eZZt+2BZyhd4qoP35OvANm3wCvMkHXsW/jFHRA4bJHo9roBus+5U183rCJAiDvhKstpi8UJr/eE9x2mO+E8PWHenfZ6T3jf6eX77hcA1ReNyoP5hIUN4GrmM5yt7IlLf6cN50oLnX0HrGTGPM++g6+5AwIHXXE7bWEDSDCf4Syql61OHtjJhc5+AtLMnfvYT/A4Bx7bnTy8Vw5x+wXYzRz6mP0Cr/EVLFiwGD0z2DssaADVmM9Qj/qlBMO9H1fXoAfzuR/zmd38DgglGIa9eD+hD+dzL95PKhAgAABAjAChDFdlevA+Qj/O6x68j1QhQCjFcDyb9w/6cn6fzftHJQKEcgzJM3nfoD/n+Zm8b1QjQAAAgBgBQkmu1pzF+wVzON/P4v2iIgFCWYbmGbxPMI/z/gzeJ6oSIJRmeNbm/YG5nP+1eX+oTIBQniFak/cFMAdq8r5QnQDhCIZpLd4P4CfzoBbvBycQIBzDUK3B+wD8zVyowfvAKQQIRzFc97L/gc+YD3vZ/5xEgHAcQ3YP+x34ijmxh/3OaQQIRzJss+xv4F7mRZb9zYkctBzv/f39ffdr6MrCBrzCfF7HfOZk7oBwPEN4DfsVeJU5sob9yukECC0YxteyP4GrmCfXsj/pwEFMO275P8/CBqxkPj/PfKYTd0Box5B+jv0GrGbOPMd+oxsHNK252vY1Cxuwg/n8NfOZrtwBoTXD+zb7B9jF/LnN/qEzBzdjuNr2i4UNqMR8/sV8ZgIHOeNMXugsbEBl5jPM4GBnrEkLnYUNOIn5DL056Bmv80JnYQNOZj5DTw5+aLbYWdSAjsxn6MOJAA0WO4saMIn5DGdzUsChi51FDcB8hhM5QeCQBc+CBvA18xnqc8JAsYXPQgZwPfMZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC+zfA/9FhVkOwYUcEAAAAASUVORK5CYII=";
|
|
3
|
+
/** Base64-encoded bike silhouette PNG. */
|
|
4
|
+
export declare const BIKE_PNG_BASE64 = "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";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Auto-generated — do not edit manually.
|
|
2
|
+
// White silhouette PNGs (800×800, RGBA) encoded as base64.
|
|
3
|
+
/** Base64-encoded scooter silhouette PNG. */
|
|
4
|
+
export const SCOOTER_PNG_BASE64 = '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';
|
|
5
|
+
/** Base64-encoded bike silhouette PNG. */
|
|
6
|
+
export const BIKE_PNG_BASE64 = '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';
|
package/lib/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { TelemetryReporter } from './telemetry/TelemetryReporter';
|
|
|
5
5
|
export { TelemetryContext } from './telemetry/TelemetryContext';
|
|
6
6
|
export { OfflineQueue } from './storage/offlineQueue';
|
|
7
7
|
export type { BundleManifest, ModelArtifact, FeatureVector, Detection as MLDetection, PolicyAST, PolicyResult, RuleResult, } from './ml/types';
|
|
8
|
-
export type { VerifyAIConfig, VerificationRequest, MultipartVerificationRequest, VerificationResult, VerificationListResponse, VerificationListParams, QueueItem, VerifyAIError, ScannerStatus, ScannerOverlayConfig, PolicyConfigResponse, } from './types';
|
|
8
|
+
export type { VerifyAIConfig, VerificationRequest, MultipartVerificationRequest, VerificationResult, VerificationListResponse, VerificationListParams, QueueItem, VerifyAIError, ScannerStatus, ScannerOverlayConfig, ScannerTheme, PolicyConfigResponse, } from './types';
|
package/lib/scanner.d.ts
CHANGED
package/lib/scanner.js
CHANGED
|
@@ -6,6 +6,8 @@ export declare class TelemetryReporter {
|
|
|
6
6
|
private apiKey;
|
|
7
7
|
private disposed;
|
|
8
8
|
private flushing;
|
|
9
|
+
private loadedPersisted;
|
|
10
|
+
private loadPersistedPromise;
|
|
9
11
|
constructor(apiKey: string, baseUrl: string);
|
|
10
12
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
11
13
|
track(eventType: string, opts?: {
|
|
@@ -21,4 +23,18 @@ export declare class TelemetryReporter {
|
|
|
21
23
|
private scheduleFlush;
|
|
22
24
|
private flushNow;
|
|
23
25
|
private clearFlushTimer;
|
|
26
|
+
/**
|
|
27
|
+
* Persist the current buffer to AsyncStorage so events survive app crashes.
|
|
28
|
+
* Fire-and-forget — never throws or blocks.
|
|
29
|
+
*
|
|
30
|
+
* IMPORTANT: skips persist if loadPersistedBuffer() hasn't completed yet,
|
|
31
|
+
* to avoid overwriting orphaned events from a crashed previous session.
|
|
32
|
+
*/
|
|
33
|
+
private persistBuffer;
|
|
34
|
+
/**
|
|
35
|
+
* Load crash-persisted events from a previous session and merge into current buffer.
|
|
36
|
+
* Runs once per instance. If orphaned events are found, emits an `sdk_crash` event
|
|
37
|
+
* so the monitor can detect and investigate app crashes.
|
|
38
|
+
*/
|
|
39
|
+
private loadPersistedBuffer;
|
|
24
40
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import { SDK_VERSION } from '../version';
|
|
3
|
+
let _storage = null;
|
|
4
|
+
async function getStorage() {
|
|
5
|
+
if (!_storage) {
|
|
6
|
+
const mod = await import('@react-native-async-storage/async-storage');
|
|
7
|
+
_storage = mod.default;
|
|
8
|
+
}
|
|
9
|
+
return _storage;
|
|
10
|
+
}
|
|
11
|
+
const TELEMETRY_PERSIST_KEY = '@verifyai/telemetry_buffer';
|
|
3
12
|
/** Event types that flush immediately (critical init failures). */
|
|
4
13
|
const CRITICAL_EVENTS = new Set([
|
|
5
14
|
'camera_init_failure',
|
|
@@ -12,9 +21,13 @@ export class TelemetryReporter {
|
|
|
12
21
|
this.flushTimer = null;
|
|
13
22
|
this.disposed = false;
|
|
14
23
|
this.flushing = false;
|
|
24
|
+
this.loadedPersisted = false;
|
|
25
|
+
this.loadPersistedPromise = null;
|
|
15
26
|
this.apiKey = apiKey;
|
|
16
27
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
17
28
|
this.sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
29
|
+
// Load any crash-persisted events from the previous session
|
|
30
|
+
this.loadPersistedBuffer();
|
|
18
31
|
}
|
|
19
32
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
20
33
|
track(eventType, opts = {}) {
|
|
@@ -59,6 +72,18 @@ export class TelemetryReporter {
|
|
|
59
72
|
else {
|
|
60
73
|
this.scheduleFlush();
|
|
61
74
|
}
|
|
75
|
+
// Persist immediately once crash recovery has completed. If startup
|
|
76
|
+
// recovery is still in flight, write the merged buffer back afterwards.
|
|
77
|
+
if (this.loadedPersisted) {
|
|
78
|
+
this.persistBuffer();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
void this.loadPersistedBuffer().then(() => {
|
|
82
|
+
if (!this.disposed && this.buffer.size > 0) {
|
|
83
|
+
this.persistBuffer();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
62
87
|
}
|
|
63
88
|
catch {
|
|
64
89
|
// Never throw from telemetry
|
|
@@ -66,6 +91,8 @@ export class TelemetryReporter {
|
|
|
66
91
|
}
|
|
67
92
|
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
68
93
|
async flush() {
|
|
94
|
+
// Merge any crash-persisted events from previous session before flushing
|
|
95
|
+
await this.loadPersistedBuffer();
|
|
69
96
|
if (this.buffer.size === 0 || this.flushing)
|
|
70
97
|
return;
|
|
71
98
|
this.flushing = true;
|
|
@@ -88,6 +115,8 @@ export class TelemetryReporter {
|
|
|
88
115
|
if (!response.ok) {
|
|
89
116
|
throw new Error(`Telemetry request failed with status ${response.status}`);
|
|
90
117
|
}
|
|
118
|
+
// Success — clear persisted buffer since events are now server-side
|
|
119
|
+
this.persistBuffer(); // buffer is empty at this point, so this clears the key
|
|
91
120
|
}
|
|
92
121
|
catch {
|
|
93
122
|
for (const [dedupKey, event] of bufferedEntries) {
|
|
@@ -138,4 +167,110 @@ export class TelemetryReporter {
|
|
|
138
167
|
this.flushTimer = null;
|
|
139
168
|
}
|
|
140
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Persist the current buffer to AsyncStorage so events survive app crashes.
|
|
172
|
+
* Fire-and-forget — never throws or blocks.
|
|
173
|
+
*
|
|
174
|
+
* IMPORTANT: skips persist if loadPersistedBuffer() hasn't completed yet,
|
|
175
|
+
* to avoid overwriting orphaned events from a crashed previous session.
|
|
176
|
+
*/
|
|
177
|
+
persistBuffer() {
|
|
178
|
+
if (!this.loadedPersisted)
|
|
179
|
+
return; // Don't overwrite orphaned crash data before it's loaded
|
|
180
|
+
try {
|
|
181
|
+
const entries = {};
|
|
182
|
+
for (const [key, event] of this.buffer) {
|
|
183
|
+
entries[key] = event;
|
|
184
|
+
}
|
|
185
|
+
getStorage().then(s => {
|
|
186
|
+
if (Object.keys(entries).length > 0) {
|
|
187
|
+
s.setItem(TELEMETRY_PERSIST_KEY, JSON.stringify(entries));
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
s.removeItem(TELEMETRY_PERSIST_KEY);
|
|
191
|
+
}
|
|
192
|
+
}).catch(() => { });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Never throw from telemetry
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Load crash-persisted events from a previous session and merge into current buffer.
|
|
200
|
+
* Runs once per instance. If orphaned events are found, emits an `sdk_crash` event
|
|
201
|
+
* so the monitor can detect and investigate app crashes.
|
|
202
|
+
*/
|
|
203
|
+
async loadPersistedBuffer() {
|
|
204
|
+
if (this.loadedPersisted)
|
|
205
|
+
return;
|
|
206
|
+
if (this.loadPersistedPromise) {
|
|
207
|
+
await this.loadPersistedPromise;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.loadPersistedPromise = (async () => {
|
|
211
|
+
try {
|
|
212
|
+
const storage = await getStorage();
|
|
213
|
+
const raw = await storage.getItem(TELEMETRY_PERSIST_KEY);
|
|
214
|
+
if (!raw)
|
|
215
|
+
return;
|
|
216
|
+
const entries = JSON.parse(raw);
|
|
217
|
+
let orphanedCount = 0;
|
|
218
|
+
const orphanedTypes = [];
|
|
219
|
+
for (const [key, event] of Object.entries(entries)) {
|
|
220
|
+
if (event.session_id === this.sessionId)
|
|
221
|
+
continue; // Same session, skip
|
|
222
|
+
orphanedCount++;
|
|
223
|
+
orphanedTypes.push(event.event_type);
|
|
224
|
+
// Merge orphaned events into the current buffer
|
|
225
|
+
const existing = this.buffer.get(key);
|
|
226
|
+
if (existing) {
|
|
227
|
+
existing.event_count += event.event_count;
|
|
228
|
+
if (event.first_occurred_at < existing.first_occurred_at) {
|
|
229
|
+
existing.first_occurred_at = event.first_occurred_at;
|
|
230
|
+
}
|
|
231
|
+
if (event.last_occurred_at > existing.last_occurred_at) {
|
|
232
|
+
existing.last_occurred_at = event.last_occurred_at;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.buffer.set(key, event);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Emit sdk_crash event if we recovered orphaned events from a dead session
|
|
240
|
+
if (orphanedCount > 0) {
|
|
241
|
+
const now = new Date().toISOString();
|
|
242
|
+
const uniqueTypes = [...new Set(orphanedTypes)];
|
|
243
|
+
const crashEvent = {
|
|
244
|
+
event_type: 'sdk_crash',
|
|
245
|
+
component: 'TelemetryReporter',
|
|
246
|
+
error_message: `Recovered ${orphanedCount} unflushed event(s) from crashed/killed session: ${uniqueTypes.join(', ')}`,
|
|
247
|
+
sdk_platform: Platform.OS,
|
|
248
|
+
sdk_version: SDK_VERSION,
|
|
249
|
+
os_name: Platform.OS,
|
|
250
|
+
os_version: String(Platform.Version),
|
|
251
|
+
session_id: this.sessionId,
|
|
252
|
+
event_count: 1,
|
|
253
|
+
first_occurred_at: now,
|
|
254
|
+
last_occurred_at: now,
|
|
255
|
+
};
|
|
256
|
+
this.buffer.set(`sdk_crash|recovered|${this.sessionId}`, crashEvent);
|
|
257
|
+
}
|
|
258
|
+
// Clear persisted data now that it's loaded into memory. The merged
|
|
259
|
+
// buffer will be re-persisted below until a flush succeeds.
|
|
260
|
+
await storage.removeItem(TELEMETRY_PERSIST_KEY);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Never throw from telemetry
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
this.loadedPersisted = true;
|
|
267
|
+
this.loadPersistedPromise = null;
|
|
268
|
+
if (this.buffer.size > 0 && !this.disposed) {
|
|
269
|
+
this.persistBuffer();
|
|
270
|
+
this.scheduleFlush();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})();
|
|
274
|
+
await this.loadPersistedPromise;
|
|
275
|
+
}
|
|
141
276
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,12 +12,16 @@ export interface VerificationRequest {
|
|
|
12
12
|
policy: string;
|
|
13
13
|
metadata?: Record<string, unknown>;
|
|
14
14
|
provider?: 'openai' | 'anthropic' | 'gemini';
|
|
15
|
+
/** When true, the response includes the image as a base64 data URI in `image_data`. */
|
|
16
|
+
include_image_data?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export interface MultipartVerificationRequest {
|
|
17
19
|
imageUri: string;
|
|
18
20
|
policy: string;
|
|
19
21
|
metadata?: Record<string, unknown>;
|
|
20
22
|
provider?: 'openai' | 'anthropic' | 'gemini';
|
|
23
|
+
/** When true, the response includes the image as a base64 data URI in `image_data`. */
|
|
24
|
+
include_image_data?: boolean;
|
|
21
25
|
}
|
|
22
26
|
export interface VerificationResult {
|
|
23
27
|
id: string;
|
|
@@ -30,6 +34,8 @@ export interface VerificationResult {
|
|
|
30
34
|
feedback: string;
|
|
31
35
|
metadata: Record<string, unknown>;
|
|
32
36
|
image_url: string | null;
|
|
37
|
+
/** Base64-encoded image data URI. Only present when `include_image_data` is requested. */
|
|
38
|
+
image_data?: string;
|
|
33
39
|
/** Classification category (e.g. good_parking, no_vehicle, poor_photo). */
|
|
34
40
|
category?: string;
|
|
35
41
|
}
|
|
@@ -70,6 +76,23 @@ export interface VerifyAIError {
|
|
|
70
76
|
method?: string;
|
|
71
77
|
}
|
|
72
78
|
export type ScannerStatus = 'idle' | 'capturing' | 'processing' | 'success' | 'error';
|
|
79
|
+
/**
|
|
80
|
+
* Theme customization for the scanner overlay.
|
|
81
|
+
*
|
|
82
|
+
* All properties are optional — unset values fall back to built-in defaults.
|
|
83
|
+
* Pass a `ScannerTheme` to `ScannerOverlayConfig.theme` to match the scanner
|
|
84
|
+
* to your app's brand.
|
|
85
|
+
*/
|
|
86
|
+
export interface ScannerTheme {
|
|
87
|
+
/** Color for success states (checkmark circle, success card accent). Default: '#22C55E'. */
|
|
88
|
+
successColor?: string;
|
|
89
|
+
/** Color for failure/error states (warning icon, error card accent). Default: '#F59E0B'. */
|
|
90
|
+
failureColor?: string;
|
|
91
|
+
/** Color for the capture button circle. Default: '#FFFFFF'. */
|
|
92
|
+
captureButtonColor?: string;
|
|
93
|
+
/** Color for the guide frame corner brackets. Default: 'rgba(255, 255, 255, 0.7)'. */
|
|
94
|
+
cornerColor?: string;
|
|
95
|
+
}
|
|
73
96
|
export interface ScannerOverlayConfig {
|
|
74
97
|
title?: string;
|
|
75
98
|
instructions?: string;
|
|
@@ -99,6 +122,8 @@ export interface ScannerOverlayConfig {
|
|
|
99
122
|
maxAttempts?: number;
|
|
100
123
|
autoApproveOnExhaust?: boolean;
|
|
101
124
|
showTechnicalErrorDetails?: boolean;
|
|
125
|
+
/** Custom theme for scanner colors. */
|
|
126
|
+
theme?: ScannerTheme;
|
|
102
127
|
}
|
|
103
128
|
export interface PolicyConfigResponse {
|
|
104
129
|
maxAttempts: number;
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.
|
|
1
|
+
export declare const SDK_VERSION = "2.4.0";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.
|
|
1
|
+
export const SDK_VERSION = '2.4.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchlabs/verify-ai-react-native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"jpeg-js": "^0.4.4"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"react": ">=
|
|
43
|
-
"react-native": ">=0.72.0",
|
|
42
|
+
"@react-native-async-storage/async-storage": ">=1.19.0",
|
|
44
43
|
"expo-camera": ">=15.0.0",
|
|
44
|
+
"expo-file-system": ">=17.0.0",
|
|
45
45
|
"expo-image-manipulator": ">=12.0.0",
|
|
46
|
-
"
|
|
47
|
-
"react-native
|
|
48
|
-
"
|
|
46
|
+
"react": ">=18.0.0",
|
|
47
|
+
"react-native": ">=0.72.0",
|
|
48
|
+
"react-native-fast-tflite": ">=1.0.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"expo-camera": {
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@types/jpeg-js": "^0.3.7",
|
|
69
68
|
"@react-native-async-storage/async-storage": "^2.1.0",
|
|
69
|
+
"@types/jpeg-js": "^0.3.7",
|
|
70
70
|
"@types/react": "^18.2.0",
|
|
71
71
|
"expo-camera": "^16.0.0",
|
|
72
72
|
"expo-file-system": "^18.0.12",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"react": "^18.2.0",
|
|
75
75
|
"react-native": "^0.76.0",
|
|
76
76
|
"react-native-fast-tflite": "^1.6.1",
|
|
77
|
-
"typescript": "^5.5.0"
|
|
77
|
+
"typescript": "^5.5.0",
|
|
78
|
+
"vitest": "^4.1.0"
|
|
78
79
|
}
|
|
79
80
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -253,6 +253,9 @@ export class VerifyAIClient {
|
|
|
253
253
|
if (request.provider) {
|
|
254
254
|
formData.append('provider', request.provider);
|
|
255
255
|
}
|
|
256
|
+
if (request.include_image_data) {
|
|
257
|
+
formData.append('include_image_data', 'true');
|
|
258
|
+
}
|
|
256
259
|
|
|
257
260
|
const headers: Record<string, string> = {
|
|
258
261
|
'X-API-Key': this.apiKey,
|
|
@@ -283,6 +286,7 @@ export class VerifyAIClient {
|
|
|
283
286
|
policy: request.policy,
|
|
284
287
|
metadata: request.metadata,
|
|
285
288
|
provider: request.provider,
|
|
289
|
+
include_image_data: request.include_image_data,
|
|
286
290
|
});
|
|
287
291
|
} catch {
|
|
288
292
|
// If the retry itself fails, throw the original error
|
|
@@ -388,7 +392,7 @@ export class VerifyAIRequestError extends Error {
|
|
|
388
392
|
}
|
|
389
393
|
|
|
390
394
|
get isRetryable(): boolean {
|
|
391
|
-
return this.status === 408 || this.status === 429 || this.status >= 500;
|
|
395
|
+
return this.status === 0 || this.status === 408 || this.status === 429 || this.status >= 500;
|
|
392
396
|
}
|
|
393
397
|
|
|
394
398
|
get upgradeUrl(): string | undefined {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Image } from 'react-native';
|
|
3
|
+
import { BIKE_PNG_BASE64 } from './overlayAssets';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in bike silhouette overlay for the scanner guide frame.
|
|
7
|
+
*
|
|
8
|
+
* Renders a white bike silhouette from an embedded PNG asset.
|
|
9
|
+
* Pass as `guideOverlayContent` in `ScannerOverlayConfig`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* overlay={{ guideOverlayContent: <BikeOverlay />, guideOverlayOpacity: 0.3 }}
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function BikeOverlay() {
|
|
17
|
+
return (
|
|
18
|
+
<Image
|
|
19
|
+
source={{ uri: `data:image/png;base64,${BIKE_PNG_BASE64}` }}
|
|
20
|
+
style={{ width: '100%', height: '100%' }}
|
|
21
|
+
resizeMode="contain"
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Image } from 'react-native';
|
|
3
|
+
import { SCOOTER_PNG_BASE64 } from './overlayAssets';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in scooter silhouette overlay for the scanner guide frame.
|
|
7
|
+
*
|
|
8
|
+
* Renders a white scooter silhouette from an embedded PNG asset.
|
|
9
|
+
* Pass as `guideOverlayContent` in `ScannerOverlayConfig`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* overlay={{ guideOverlayContent: <ScooterOverlay />, guideOverlayOpacity: 0.3 }}
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function ScooterOverlay() {
|
|
17
|
+
return (
|
|
18
|
+
<Image
|
|
19
|
+
source={{ uri: `data:image/png;base64,${SCOOTER_PNG_BASE64}` }}
|
|
20
|
+
style={{ width: '100%', height: '100%' }}
|
|
21
|
+
resizeMode="contain"
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -20,11 +20,11 @@ import { useTelemetry } from '../telemetry/TelemetryContext';
|
|
|
20
20
|
import type { TelemetryReporter } from '../telemetry/TelemetryReporter';
|
|
21
21
|
|
|
22
22
|
/** Quality used when expo-image-manipulator is not available (lower = smaller). */
|
|
23
|
-
const FALLBACK_QUALITY = 0.
|
|
23
|
+
const FALLBACK_QUALITY = 0.65;
|
|
24
24
|
/** Quality used when expo-image-manipulator IS available (resize handles size). */
|
|
25
|
-
const MANIPULATOR_QUALITY = 0.
|
|
25
|
+
const MANIPULATOR_QUALITY = 0.8;
|
|
26
26
|
/** Max dimension (px) on longest side when resize is available. */
|
|
27
|
-
const MAX_DIMENSION =
|
|
27
|
+
const MAX_DIMENSION = 1600;
|
|
28
28
|
|
|
29
29
|
export interface VerifyAIScannerProps {
|
|
30
30
|
/** Called with base64 image data when the user captures a photo. */
|
|
@@ -147,21 +147,24 @@ export function VerifyAIScanner({
|
|
|
147
147
|
const [exhausted, setExhausted] = useState(false);
|
|
148
148
|
const [terminated, setTerminated] = useState(false);
|
|
149
149
|
const [cameraReady, setCameraReady] = useState(false);
|
|
150
|
+
const [cameraKey, setCameraKey] = useState(0);
|
|
150
151
|
const cameraReadyRef = useRef(false);
|
|
151
152
|
const cameraInitFailedRef = useRef(false);
|
|
152
153
|
const permissionDeniedTrackedRef = useRef(false);
|
|
153
154
|
|
|
155
|
+
const pausePreview = useCallback(() => {
|
|
156
|
+
cameraRef.current?.pausePreview?.().catch(() => {});
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
154
159
|
// Release camera (and torch) when a terminal result is reached or on unmount.
|
|
155
160
|
const releaseCamera = useCallback(() => {
|
|
156
161
|
setTerminated(true);
|
|
157
|
-
|
|
158
|
-
}, []);
|
|
162
|
+
pausePreview();
|
|
163
|
+
}, [pausePreview]);
|
|
159
164
|
|
|
160
165
|
useEffect(() => {
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
}, []);
|
|
166
|
+
return pausePreview;
|
|
167
|
+
}, [pausePreview]);
|
|
165
168
|
|
|
166
169
|
// Camera init callbacks
|
|
167
170
|
const onCameraReady = useCallback(() => {
|
|
@@ -185,7 +188,10 @@ export function VerifyAIScanner({
|
|
|
185
188
|
});
|
|
186
189
|
}, [onError, telemetry]);
|
|
187
190
|
|
|
188
|
-
// Startup watchdog — if camera hasn't fired onCameraReady within
|
|
191
|
+
// Startup watchdog — if camera hasn't fired onCameraReady within 3s, force remount.
|
|
192
|
+
// On iOS the native AVCaptureSession sometimes fails to start its preview layer on first
|
|
193
|
+
// mount (e.g. during navigation transitions). Changing the key forces React to destroy and
|
|
194
|
+
// recreate the CameraView, which starts a fresh native session.
|
|
189
195
|
useEffect(() => {
|
|
190
196
|
if (!permission?.granted || terminated) return;
|
|
191
197
|
|
|
@@ -193,13 +199,17 @@ export function VerifyAIScanner({
|
|
|
193
199
|
if (!cameraReadyRef.current && !cameraInitFailedRef.current) {
|
|
194
200
|
telemetry?.track('camera_preview_timeout', {
|
|
195
201
|
component: 'scanner',
|
|
196
|
-
error: 'Camera did not initialize within
|
|
202
|
+
error: 'Camera did not initialize within 3 seconds — remounting',
|
|
197
203
|
});
|
|
204
|
+
// Reset state and bump key to force a fresh CameraView mount
|
|
205
|
+
setCameraReady(false);
|
|
206
|
+
cameraReadyRef.current = false;
|
|
207
|
+
setCameraKey((k) => k + 1);
|
|
198
208
|
}
|
|
199
|
-
},
|
|
209
|
+
}, 3000);
|
|
200
210
|
|
|
201
211
|
return () => clearTimeout(timer);
|
|
202
|
-
}, [permission?.granted, terminated, telemetry]);
|
|
212
|
+
}, [permission?.granted, terminated, cameraKey, telemetry]);
|
|
203
213
|
|
|
204
214
|
// Track permission denied
|
|
205
215
|
useEffect(() => {
|
|
@@ -419,7 +429,7 @@ export function VerifyAIScanner({
|
|
|
419
429
|
|
|
420
430
|
return (
|
|
421
431
|
<View style={[styles.container, style]}>
|
|
422
|
-
<CameraView ref={cameraRef} style={styles.camera} facing="back" enableTorch={!terminated && enableTorch} onCameraReady={onCameraReady} onMountError={onMountError}>
|
|
432
|
+
<CameraView key={cameraKey} ref={cameraRef} style={styles.camera} facing="back" enableTorch={!terminated && enableTorch} onCameraReady={onCameraReady} onMountError={onMountError}>
|
|
423
433
|
{/* Overlay */}
|
|
424
434
|
<View style={styles.overlay}>
|
|
425
435
|
{overlay?.title && (
|
|
@@ -445,10 +455,10 @@ export function VerifyAIScanner({
|
|
|
445
455
|
</View>
|
|
446
456
|
)}
|
|
447
457
|
{/* Corner brackets */}
|
|
448
|
-
<View style={[styles.corner, styles.cornerTopLeft]} />
|
|
449
|
-
<View style={[styles.corner, styles.cornerTopRight]} />
|
|
450
|
-
<View style={[styles.corner, styles.cornerBottomLeft]} />
|
|
451
|
-
<View style={[styles.corner, styles.cornerBottomRight]} />
|
|
458
|
+
<View style={[styles.corner, styles.cornerTopLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined]} />
|
|
459
|
+
<View style={[styles.corner, styles.cornerTopRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined]} />
|
|
460
|
+
<View style={[styles.corner, styles.cornerBottomLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined]} />
|
|
461
|
+
<View style={[styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined]} />
|
|
452
462
|
</View>
|
|
453
463
|
{overlay.guideCaption && (
|
|
454
464
|
<Text style={styles.guideCaptionText}>{overlay.guideCaption}</Text>
|
|
@@ -478,6 +488,8 @@ export function VerifyAIScanner({
|
|
|
478
488
|
style={[
|
|
479
489
|
styles.resultIconCircle,
|
|
480
490
|
result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
|
|
491
|
+
result.is_compliant && overlay?.theme?.successColor ? { backgroundColor: overlay.theme.successColor } : undefined,
|
|
492
|
+
!result.is_compliant && overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined,
|
|
481
493
|
]}
|
|
482
494
|
>
|
|
483
495
|
<Text style={styles.resultIcon}>
|
|
@@ -488,6 +500,8 @@ export function VerifyAIScanner({
|
|
|
488
500
|
style={[
|
|
489
501
|
styles.resultLabel,
|
|
490
502
|
result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
|
|
503
|
+
result.is_compliant && overlay?.theme?.successColor ? { color: overlay.theme.successColor } : undefined,
|
|
504
|
+
!result.is_compliant && overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined,
|
|
491
505
|
]}
|
|
492
506
|
>
|
|
493
507
|
{result.is_compliant
|
|
@@ -526,10 +540,10 @@ export function VerifyAIScanner({
|
|
|
526
540
|
return (
|
|
527
541
|
<View style={styles.resultCard}>
|
|
528
542
|
<View style={styles.resultCardHeader}>
|
|
529
|
-
<View style={[styles.resultIconCircle, styles.resultIconError]}>
|
|
543
|
+
<View style={[styles.resultIconCircle, styles.resultIconError, overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined]}>
|
|
530
544
|
<Text style={styles.resultIcon}>!</Text>
|
|
531
545
|
</View>
|
|
532
|
-
<Text style={[styles.resultLabel, styles.resultLabelError]}>
|
|
546
|
+
<Text style={[styles.resultLabel, styles.resultLabelError, overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined]}>
|
|
533
547
|
{errorTitle}
|
|
534
548
|
</Text>
|
|
535
549
|
</View>
|
|
@@ -550,6 +564,7 @@ export function VerifyAIScanner({
|
|
|
550
564
|
<TouchableOpacity
|
|
551
565
|
style={[
|
|
552
566
|
styles.captureButton,
|
|
567
|
+
overlay?.theme?.captureButtonColor ? { borderColor: overlay.theme.captureButtonColor } : undefined,
|
|
553
568
|
(!cameraReady || status === 'capturing' || status === 'processing') &&
|
|
554
569
|
styles.captureButtonDisabled,
|
|
555
570
|
]}
|
|
@@ -557,7 +572,7 @@ export function VerifyAIScanner({
|
|
|
557
572
|
disabled={!cameraReady || status === 'capturing' || status === 'processing'}
|
|
558
573
|
activeOpacity={0.7}
|
|
559
574
|
>
|
|
560
|
-
<View style={styles.captureButtonInner} />
|
|
575
|
+
<View style={[styles.captureButtonInner, overlay?.theme?.captureButtonColor ? { backgroundColor: overlay.theme.captureButtonColor } : undefined]} />
|
|
561
576
|
</TouchableOpacity>
|
|
562
577
|
</View>
|
|
563
578
|
)}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Auto-generated — do not edit manually.
|
|
2
|
+
// White silhouette PNGs (800×800, RGBA) encoded as base64.
|
|
3
|
+
|
|
4
|
+
/** Base64-encoded scooter silhouette PNG. */
|
|
5
|
+
export const SCOOTER_PNG_BASE64 = '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';
|
|
6
|
+
|
|
7
|
+
/** Base64-encoded bike silhouette PNG. */
|
|
8
|
+
export const BIKE_PNG_BASE64 = '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';
|
package/src/index.ts
CHANGED
package/src/scanner.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import { SDK_VERSION } from '../version';
|
|
3
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
|
+
|
|
13
|
+
const TELEMETRY_PERSIST_KEY = '@verifyai/telemetry_buffer';
|
|
14
|
+
|
|
4
15
|
interface TelemetryEvent {
|
|
5
16
|
event_type: string;
|
|
6
17
|
component?: string;
|
|
@@ -38,11 +49,15 @@ export class TelemetryReporter {
|
|
|
38
49
|
private apiKey: string;
|
|
39
50
|
private disposed = false;
|
|
40
51
|
private flushing = false;
|
|
52
|
+
private loadedPersisted = false;
|
|
53
|
+
private loadPersistedPromise: Promise<void> | null = null;
|
|
41
54
|
|
|
42
55
|
constructor(apiKey: string, baseUrl: string) {
|
|
43
56
|
this.apiKey = apiKey;
|
|
44
57
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
45
58
|
this.sessionId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
59
|
+
// Load any crash-persisted events from the previous session
|
|
60
|
+
this.loadPersistedBuffer();
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
/** Track an error event. Fire-and-forget — never throws. */
|
|
@@ -100,6 +115,18 @@ export class TelemetryReporter {
|
|
|
100
115
|
} else {
|
|
101
116
|
this.scheduleFlush();
|
|
102
117
|
}
|
|
118
|
+
|
|
119
|
+
// Persist immediately once crash recovery has completed. If startup
|
|
120
|
+
// recovery is still in flight, write the merged buffer back afterwards.
|
|
121
|
+
if (this.loadedPersisted) {
|
|
122
|
+
this.persistBuffer();
|
|
123
|
+
} else {
|
|
124
|
+
void this.loadPersistedBuffer().then(() => {
|
|
125
|
+
if (!this.disposed && this.buffer.size > 0) {
|
|
126
|
+
this.persistBuffer();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
103
130
|
} catch {
|
|
104
131
|
// Never throw from telemetry
|
|
105
132
|
}
|
|
@@ -107,6 +134,9 @@ export class TelemetryReporter {
|
|
|
107
134
|
|
|
108
135
|
/** Flush all buffered events immediately. Returns a promise but never rejects. */
|
|
109
136
|
async flush(): Promise<void> {
|
|
137
|
+
// Merge any crash-persisted events from previous session before flushing
|
|
138
|
+
await this.loadPersistedBuffer();
|
|
139
|
+
|
|
110
140
|
if (this.buffer.size === 0 || this.flushing) return;
|
|
111
141
|
|
|
112
142
|
this.flushing = true;
|
|
@@ -131,6 +161,9 @@ export class TelemetryReporter {
|
|
|
131
161
|
if (!response.ok) {
|
|
132
162
|
throw new Error(`Telemetry request failed with status ${response.status}`);
|
|
133
163
|
}
|
|
164
|
+
|
|
165
|
+
// Success — clear persisted buffer since events are now server-side
|
|
166
|
+
this.persistBuffer(); // buffer is empty at this point, so this clears the key
|
|
134
167
|
} catch {
|
|
135
168
|
for (const [dedupKey, event] of bufferedEntries) {
|
|
136
169
|
const existing = this.buffer.get(dedupKey);
|
|
@@ -181,4 +214,109 @@ export class TelemetryReporter {
|
|
|
181
214
|
this.flushTimer = null;
|
|
182
215
|
}
|
|
183
216
|
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Persist the current buffer to AsyncStorage so events survive app crashes.
|
|
220
|
+
* Fire-and-forget — never throws or blocks.
|
|
221
|
+
*
|
|
222
|
+
* IMPORTANT: skips persist if loadPersistedBuffer() hasn't completed yet,
|
|
223
|
+
* to avoid overwriting orphaned events from a crashed previous session.
|
|
224
|
+
*/
|
|
225
|
+
private persistBuffer(): void {
|
|
226
|
+
if (!this.loadedPersisted) return; // Don't overwrite orphaned crash data before it's loaded
|
|
227
|
+
try {
|
|
228
|
+
const entries: Record<string, TelemetryEvent> = {};
|
|
229
|
+
for (const [key, event] of this.buffer) {
|
|
230
|
+
entries[key] = event;
|
|
231
|
+
}
|
|
232
|
+
getStorage().then(s => {
|
|
233
|
+
if (Object.keys(entries).length > 0) {
|
|
234
|
+
s.setItem(TELEMETRY_PERSIST_KEY, JSON.stringify(entries));
|
|
235
|
+
} else {
|
|
236
|
+
s.removeItem(TELEMETRY_PERSIST_KEY);
|
|
237
|
+
}
|
|
238
|
+
}).catch(() => { /* never throw from telemetry */ });
|
|
239
|
+
} catch {
|
|
240
|
+
// Never throw from telemetry
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Load crash-persisted events from a previous session and merge into current buffer.
|
|
246
|
+
* Runs once per instance. If orphaned events are found, emits an `sdk_crash` event
|
|
247
|
+
* so the monitor can detect and investigate app crashes.
|
|
248
|
+
*/
|
|
249
|
+
private async loadPersistedBuffer(): Promise<void> {
|
|
250
|
+
if (this.loadedPersisted) return;
|
|
251
|
+
if (this.loadPersistedPromise) {
|
|
252
|
+
await this.loadPersistedPromise;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.loadPersistedPromise = (async () => {
|
|
257
|
+
try {
|
|
258
|
+
const storage = await getStorage();
|
|
259
|
+
const raw = await storage.getItem(TELEMETRY_PERSIST_KEY);
|
|
260
|
+
if (!raw) return;
|
|
261
|
+
|
|
262
|
+
const entries = JSON.parse(raw) as Record<string, TelemetryEvent>;
|
|
263
|
+
let orphanedCount = 0;
|
|
264
|
+
const orphanedTypes: string[] = [];
|
|
265
|
+
|
|
266
|
+
for (const [key, event] of Object.entries(entries)) {
|
|
267
|
+
if (event.session_id === this.sessionId) continue; // Same session, skip
|
|
268
|
+
orphanedCount++;
|
|
269
|
+
orphanedTypes.push(event.event_type);
|
|
270
|
+
// Merge orphaned events into the current buffer
|
|
271
|
+
const existing = this.buffer.get(key);
|
|
272
|
+
if (existing) {
|
|
273
|
+
existing.event_count += event.event_count;
|
|
274
|
+
if (event.first_occurred_at < existing.first_occurred_at) {
|
|
275
|
+
existing.first_occurred_at = event.first_occurred_at;
|
|
276
|
+
}
|
|
277
|
+
if (event.last_occurred_at > existing.last_occurred_at) {
|
|
278
|
+
existing.last_occurred_at = event.last_occurred_at;
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
this.buffer.set(key, event);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Emit sdk_crash event if we recovered orphaned events from a dead session
|
|
286
|
+
if (orphanedCount > 0) {
|
|
287
|
+
const now = new Date().toISOString();
|
|
288
|
+
const uniqueTypes = [...new Set(orphanedTypes)];
|
|
289
|
+
const crashEvent: TelemetryEvent = {
|
|
290
|
+
event_type: 'sdk_crash',
|
|
291
|
+
component: 'TelemetryReporter',
|
|
292
|
+
error_message: `Recovered ${orphanedCount} unflushed event(s) from crashed/killed session: ${uniqueTypes.join(', ')}`,
|
|
293
|
+
sdk_platform: Platform.OS,
|
|
294
|
+
sdk_version: SDK_VERSION,
|
|
295
|
+
os_name: Platform.OS,
|
|
296
|
+
os_version: String(Platform.Version),
|
|
297
|
+
session_id: this.sessionId,
|
|
298
|
+
event_count: 1,
|
|
299
|
+
first_occurred_at: now,
|
|
300
|
+
last_occurred_at: now,
|
|
301
|
+
};
|
|
302
|
+
this.buffer.set(`sdk_crash|recovered|${this.sessionId}`, crashEvent);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Clear persisted data now that it's loaded into memory. The merged
|
|
306
|
+
// buffer will be re-persisted below until a flush succeeds.
|
|
307
|
+
await storage.removeItem(TELEMETRY_PERSIST_KEY);
|
|
308
|
+
} catch {
|
|
309
|
+
// Never throw from telemetry
|
|
310
|
+
} finally {
|
|
311
|
+
this.loadedPersisted = true;
|
|
312
|
+
this.loadPersistedPromise = null;
|
|
313
|
+
if (this.buffer.size > 0 && !this.disposed) {
|
|
314
|
+
this.persistBuffer();
|
|
315
|
+
this.scheduleFlush();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
|
|
320
|
+
await this.loadPersistedPromise;
|
|
321
|
+
}
|
|
184
322
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface VerificationRequest {
|
|
|
14
14
|
policy: string;
|
|
15
15
|
metadata?: Record<string, unknown>;
|
|
16
16
|
provider?: 'openai' | 'anthropic' | 'gemini';
|
|
17
|
+
/** When true, the response includes the image as a base64 data URI in `image_data`. */
|
|
18
|
+
include_image_data?: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export interface MultipartVerificationRequest {
|
|
@@ -21,6 +23,8 @@ export interface MultipartVerificationRequest {
|
|
|
21
23
|
policy: string;
|
|
22
24
|
metadata?: Record<string, unknown>;
|
|
23
25
|
provider?: 'openai' | 'anthropic' | 'gemini';
|
|
26
|
+
/** When true, the response includes the image as a base64 data URI in `image_data`. */
|
|
27
|
+
include_image_data?: boolean;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export interface VerificationResult {
|
|
@@ -34,6 +38,8 @@ export interface VerificationResult {
|
|
|
34
38
|
feedback: string;
|
|
35
39
|
metadata: Record<string, unknown>;
|
|
36
40
|
image_url: string | null;
|
|
41
|
+
/** Base64-encoded image data URI. Only present when `include_image_data` is requested. */
|
|
42
|
+
image_data?: string;
|
|
37
43
|
/** Classification category (e.g. good_parking, no_vehicle, poor_photo). */
|
|
38
44
|
category?: string;
|
|
39
45
|
}
|
|
@@ -81,6 +87,24 @@ export interface VerifyAIError {
|
|
|
81
87
|
|
|
82
88
|
export type ScannerStatus = 'idle' | 'capturing' | 'processing' | 'success' | 'error';
|
|
83
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Theme customization for the scanner overlay.
|
|
92
|
+
*
|
|
93
|
+
* All properties are optional — unset values fall back to built-in defaults.
|
|
94
|
+
* Pass a `ScannerTheme` to `ScannerOverlayConfig.theme` to match the scanner
|
|
95
|
+
* to your app's brand.
|
|
96
|
+
*/
|
|
97
|
+
export interface ScannerTheme {
|
|
98
|
+
/** Color for success states (checkmark circle, success card accent). Default: '#22C55E'. */
|
|
99
|
+
successColor?: string;
|
|
100
|
+
/** Color for failure/error states (warning icon, error card accent). Default: '#F59E0B'. */
|
|
101
|
+
failureColor?: string;
|
|
102
|
+
/** Color for the capture button circle. Default: '#FFFFFF'. */
|
|
103
|
+
captureButtonColor?: string;
|
|
104
|
+
/** Color for the guide frame corner brackets. Default: 'rgba(255, 255, 255, 0.7)'. */
|
|
105
|
+
cornerColor?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
export interface ScannerOverlayConfig {
|
|
85
109
|
title?: string;
|
|
86
110
|
instructions?: string;
|
|
@@ -110,6 +134,8 @@ export interface ScannerOverlayConfig {
|
|
|
110
134
|
maxAttempts?: number;
|
|
111
135
|
autoApproveOnExhaust?: boolean;
|
|
112
136
|
showTechnicalErrorDetails?: boolean;
|
|
137
|
+
/** Custom theme for scanner colors. */
|
|
138
|
+
theme?: ScannerTheme;
|
|
113
139
|
}
|
|
114
140
|
|
|
115
141
|
export interface PolicyConfigResponse {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.
|
|
1
|
+
export const SDK_VERSION = '2.4.0';
|