@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.
@@ -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.5;
7
+ const FALLBACK_QUALITY = 0.65;
8
8
  /** Quality used when expo-image-manipulator IS available (resize handles size). */
9
- const MANIPULATOR_QUALITY = 0.7;
9
+ const MANIPULATOR_QUALITY = 0.8;
10
10
  /** Max dimension (px) on longest side when resize is available. */
11
- const MAX_DIMENSION = 2048;
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
- cameraRef.current?.pausePreview?.().catch(() => { });
95
- }, []);
98
+ pausePreview();
99
+ }, [pausePreview]);
96
100
  useEffect(() => {
97
- return () => {
98
- cameraRef.current?.pausePreview?.().catch(() => { });
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 5s, report timeout
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 5 seconds',
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
- }, 5000);
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 = "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";
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
@@ -1,2 +1,4 @@
1
1
  export { VerifyAIScanner } from './components/VerifyAIScanner';
2
2
  export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
3
+ export { BikeOverlay } from './components/BikeOverlay';
4
+ export { ScooterOverlay } from './components/ScooterOverlay';
package/lib/scanner.js CHANGED
@@ -1 +1,3 @@
1
1
  export { VerifyAIScanner } from './components/VerifyAIScanner';
2
+ export { BikeOverlay } from './components/BikeOverlay';
3
+ export { ScooterOverlay } from './components/ScooterOverlay';
@@ -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
  }
@@ -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.3.1";
1
+ export declare const SDK_VERSION = "2.4.0";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.3.1';
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.1",
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": ">=18.0.0",
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
- "@react-native-async-storage/async-storage": ">=1.19.0",
47
- "react-native-fast-tflite": ">=1.0.0",
48
- "expo-file-system": ">=17.0.0"
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
  }
@@ -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.5;
23
+ const FALLBACK_QUALITY = 0.65;
24
24
  /** Quality used when expo-image-manipulator IS available (resize handles size). */
25
- const MANIPULATOR_QUALITY = 0.7;
25
+ const MANIPULATOR_QUALITY = 0.8;
26
26
  /** Max dimension (px) on longest side when resize is available. */
27
- const MAX_DIMENSION = 2048;
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
- cameraRef.current?.pausePreview?.().catch(() => {});
158
- }, []);
162
+ pausePreview();
163
+ }, [pausePreview]);
159
164
 
160
165
  useEffect(() => {
161
- return () => {
162
- cameraRef.current?.pausePreview?.().catch(() => {});
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 5s, report timeout
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 5 seconds',
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
- }, 5000);
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
@@ -38,5 +38,6 @@ export type {
38
38
  VerifyAIError,
39
39
  ScannerStatus,
40
40
  ScannerOverlayConfig,
41
+ ScannerTheme,
41
42
  PolicyConfigResponse,
42
43
  } from './types';
package/src/scanner.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { VerifyAIScanner } from './components/VerifyAIScanner';
2
2
  export type { VerifyAIScannerProps } from './components/VerifyAIScanner';
3
+ export { BikeOverlay } from './components/BikeOverlay';
4
+ export { ScooterOverlay } from './components/ScooterOverlay';
@@ -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
  }
@@ -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.3.1';
1
+ export const SDK_VERSION = '2.4.0';