@switchlabs/verify-ai-react-native 2.5.2 → 2.5.4

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.
@@ -8,6 +8,7 @@ import { SDK_VERSION } from '../version';
8
8
  import { BikeOverlay } from './BikeOverlay';
9
9
  import { ScooterOverlay } from './ScooterOverlay';
10
10
  import { ANDROID_ACCELEROMETER_AXIS_DOMINANCE_THRESHOLD, classifyAndroidAccelerometerOrientation, getOverlayRotationDeg, } from './scannerOrientation';
11
+ import { CAMERA_CAPTURE_RETRY_DELAY_MS, getCameraCaptureRetryPlan, } from './cameraCaptureRetry';
11
12
  /** Quality used when expo-image-manipulator is not available (lower = smaller). */
12
13
  const FALLBACK_QUALITY = 0.65;
13
14
  /** Quality used when expo-image-manipulator IS available (resize handles size). */
@@ -19,14 +20,18 @@ const CAMERA_NOT_READY_ERROR_CODE = 'ERR_CAMERA_NOT_READY';
19
20
  const CAMERA_INIT_ERROR_CODE = 'ERR_CAMERA_INIT_FAILED';
20
21
  const DEFAULT_TERMINAL_RESULT_DISPLAY_MS = 3000;
21
22
  const TRANSIENT_ERROR_DISPLAY_MS = 3000;
22
- const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
23
- const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
24
- const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
25
- const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
26
- const IOS_CAMERA_STARTUP_WATCHDOG_MS = 8000;
23
+ // iOS AVCaptureSession cold start can legitimately exceed 8s on first launch (permission
24
+ // prompt, first-ever session spin-up, thermal/low-power throttling). An 8s watchdog tore
25
+ // down sessions that were seconds from ready and restarted from scratch, which made slow
26
+ // cold starts *worse* and fired camera_preview_timeout in bursts. Match Android at 10s.
27
+ const IOS_CAMERA_STARTUP_WATCHDOG_MS = 10000;
27
28
  const ANDROID_CAMERA_STARTUP_WATCHDOG_MS = 10000;
28
29
  const CAMERA_STARTUP_SLOW_TELEMETRY_MS = 3000;
30
+ // Base backoff between startup remounts. Each successive remount escalates this (see
31
+ // startupRemountBackoffMs below) so later attempts get progressively more uninterrupted
32
+ // time to initialize the native session instead of thrashing at a fixed interval.
29
33
  const CAMERA_STARTUP_REMOUNT_BACKOFF_MS = 350;
34
+ const CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS = 1200;
30
35
  const CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS = 5;
31
36
  const ANDROID_ORIENTATION_NO_EVENT_TIMEOUT_MS = 3000;
32
37
  const ANDROID_ORIENTATION_SAMPLE_TELEMETRY_MS = 3000;
@@ -844,6 +849,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
844
849
  const startupWatchdogRemountCount = startupWatchdogRemountCountRef.current + 1;
845
850
  startupWatchdogRemountCountRef.current = startupWatchdogRemountCount;
846
851
  cameraRemountCountRef.current++;
852
+ // Escalate the backoff with each attempt (350, 700, 1050, capped at 1200ms) so a
853
+ // camera that just needs more time isn't repeatedly interrupted at a fixed cadence.
854
+ const startupRemountBackoffMs = Math.min(CAMERA_STARTUP_REMOUNT_BACKOFF_MS * startupWatchdogRemountCount, CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS);
847
855
  telemetry?.track('camera_preview_timeout', {
848
856
  component: 'scanner',
849
857
  error: `Camera did not initialize within ${Math.round(watchdogMs / 1000)} seconds — remounting`,
@@ -852,12 +860,12 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
852
860
  remount_reason: 'startup_watchdog',
853
861
  startup_watchdog_remount_count: startupWatchdogRemountCount,
854
862
  startup_watchdog_max_remounts: CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS,
855
- startup_remount_backoff_ms: CAMERA_STARTUP_REMOUNT_BACKOFF_MS,
863
+ startup_remount_backoff_ms: startupRemountBackoffMs,
856
864
  torch_deferred_until_ready: enableTorch ? 1 : 0,
857
865
  }),
858
866
  });
859
867
  requestCameraRemount('startup_watchdog', {
860
- backoffMs: CAMERA_STARTUP_REMOUNT_BACKOFF_MS,
868
+ backoffMs: startupRemountBackoffMs,
861
869
  log: true,
862
870
  });
863
871
  }
@@ -947,6 +955,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
947
955
  let captureRetryTorchSuppressed = false;
948
956
  let captureRetrySettleDelayMs = CAMERA_CAPTURE_RETRY_DELAY_MS;
949
957
  let captureRetryRemountBackoffMs = 0;
958
+ let captureRetryReadyTimeoutMs = 0;
950
959
  let lastNativeCaptureErrorMessage = null;
951
960
  try {
952
961
  const capturePhysicalOrientation = physicalOrientation;
@@ -1014,13 +1023,11 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1014
1023
  lastNativeCaptureErrorMessage = normalized.message;
1015
1024
  if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
1016
1025
  captureRetryAttempted = true;
1017
- captureRetryTorchSuppressed = !!enableTorch;
1018
- captureRetrySettleDelayMs = captureRetryTorchSuppressed
1019
- ? CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS
1020
- : CAMERA_CAPTURE_RETRY_DELAY_MS;
1021
- captureRetryRemountBackoffMs = captureRetryTorchSuppressed
1022
- ? CAMERA_CAPTURE_RETRY_DELAY_MS
1023
- : 0;
1026
+ const retryPlan = getCameraCaptureRetryPlan(Platform.OS, !!enableTorch);
1027
+ captureRetryTorchSuppressed = retryPlan.suppressTorch;
1028
+ captureRetrySettleDelayMs = retryPlan.settleDelayMs;
1029
+ captureRetryRemountBackoffMs = retryPlan.remountBackoffMs;
1030
+ captureRetryReadyTimeoutMs = retryPlan.readyTimeoutMs;
1024
1031
  const retryAt = new Date().toISOString();
1025
1032
  lastCaptureRetryAtRef.current = retryAt;
1026
1033
  cameraRemountCountRef.current++;
@@ -1031,9 +1038,6 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1031
1038
  ? { backoffMs: captureRetryRemountBackoffMs }
1032
1039
  : undefined);
1033
1040
  await sleep(captureRetrySettleDelayMs);
1034
- const captureRetryReadyTimeoutMs = Platform.OS === 'android'
1035
- ? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
1036
- : IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
1037
1041
  captureRetryReady = await waitForCameraReady(captureRetryReadyTimeoutMs);
1038
1042
  telemetry?.track('camera_capture_retry', {
1039
1043
  component: 'scanner',
@@ -1131,6 +1135,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1131
1135
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
1132
1136
  capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
1133
1137
  capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
1138
+ capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
1134
1139
  recovered_native_capture_failure: captureRetryAttempted ? 1 : 0,
1135
1140
  last_native_capture_error: lastNativeCaptureErrorMessage,
1136
1141
  }),
@@ -1207,6 +1212,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1207
1212
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
1208
1213
  capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
1209
1214
  capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
1215
+ capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
1210
1216
  is_native_camera_capture_error: isNativeCameraCaptureError(error),
1211
1217
  last_native_capture_error: lastNativeCaptureErrorMessage,
1212
1218
  }),
@@ -0,0 +1,12 @@
1
+ export declare const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
2
+ export declare const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
3
+ export declare const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
4
+ export declare const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
5
+ export type CameraCaptureRetryPlatform = 'ios' | 'android' | string;
6
+ export interface CameraCaptureRetryPlan {
7
+ readyTimeoutMs: number;
8
+ remountBackoffMs: number;
9
+ settleDelayMs: number;
10
+ suppressTorch: boolean;
11
+ }
12
+ export declare function getCameraCaptureRetryPlan(platform: CameraCaptureRetryPlatform, torchRequested: boolean): CameraCaptureRetryPlan;
@@ -0,0 +1,23 @@
1
+ export const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
2
+ export const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
3
+ export const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
4
+ export const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
5
+ export function getCameraCaptureRetryPlan(platform, torchRequested) {
6
+ const suppressTorch = torchRequested;
7
+ return {
8
+ readyTimeoutMs: platform === 'android'
9
+ ? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
10
+ : IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS,
11
+ // Flutter fully restarts its CameraController after native capture errors.
12
+ // On iOS/Expo Camera, a key-only remount can report ready too quickly while
13
+ // the native capture session is still unable to produce a photo, so force a
14
+ // brief unmount before the retry.
15
+ remountBackoffMs: platform === 'ios' || suppressTorch
16
+ ? CAMERA_CAPTURE_RETRY_DELAY_MS
17
+ : 0,
18
+ settleDelayMs: suppressTorch
19
+ ? CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS
20
+ : CAMERA_CAPTURE_RETRY_DELAY_MS,
21
+ suppressTorch,
22
+ };
23
+ }
@@ -33,6 +33,8 @@ export interface VerificationResult {
33
33
  policy: string;
34
34
  violation_reasons: string[];
35
35
  feedback: string;
36
+ warning_violation_reasons?: string[];
37
+ warning_feedback?: string | null;
36
38
  metadata: Record<string, unknown>;
37
39
  image_url: string | null;
38
40
  /** Base64-encoded image data URI. Only present when `include_image_data` is requested. */
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.5.2";
1
+ export declare const SDK_VERSION = "2.5.4";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.5.2';
1
+ export const SDK_VERSION = '2.5.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
4
4
  "description": "React Native SDK for Verify AI - photo verification with AI vision processing",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,6 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "typecheck": "tsc --noEmit",
28
+ "test": "vitest run",
28
29
  "build": "tsc",
29
30
  "prepublishOnly": "npm run build"
30
31
  },
@@ -32,6 +32,10 @@ import {
32
32
  getOverlayRotationDeg,
33
33
  type ScannerPhysicalOrientation,
34
34
  } from './scannerOrientation';
35
+ import {
36
+ CAMERA_CAPTURE_RETRY_DELAY_MS,
37
+ getCameraCaptureRetryPlan,
38
+ } from './cameraCaptureRetry';
35
39
 
36
40
  /** Quality used when expo-image-manipulator is not available (lower = smaller). */
37
41
  const FALLBACK_QUALITY = 0.65;
@@ -44,14 +48,18 @@ const CAMERA_NOT_READY_ERROR_CODE = 'ERR_CAMERA_NOT_READY';
44
48
  const CAMERA_INIT_ERROR_CODE = 'ERR_CAMERA_INIT_FAILED';
45
49
  const DEFAULT_TERMINAL_RESULT_DISPLAY_MS = 3000;
46
50
  const TRANSIENT_ERROR_DISPLAY_MS = 3000;
47
- const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
48
- const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
49
- const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
50
- const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
51
- const IOS_CAMERA_STARTUP_WATCHDOG_MS = 8000;
51
+ // iOS AVCaptureSession cold start can legitimately exceed 8s on first launch (permission
52
+ // prompt, first-ever session spin-up, thermal/low-power throttling). An 8s watchdog tore
53
+ // down sessions that were seconds from ready and restarted from scratch, which made slow
54
+ // cold starts *worse* and fired camera_preview_timeout in bursts. Match Android at 10s.
55
+ const IOS_CAMERA_STARTUP_WATCHDOG_MS = 10000;
52
56
  const ANDROID_CAMERA_STARTUP_WATCHDOG_MS = 10000;
53
57
  const CAMERA_STARTUP_SLOW_TELEMETRY_MS = 3000;
58
+ // Base backoff between startup remounts. Each successive remount escalates this (see
59
+ // startupRemountBackoffMs below) so later attempts get progressively more uninterrupted
60
+ // time to initialize the native session instead of thrashing at a fixed interval.
54
61
  const CAMERA_STARTUP_REMOUNT_BACKOFF_MS = 350;
62
+ const CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS = 1200;
55
63
  const CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS = 5;
56
64
  const ANDROID_ORIENTATION_NO_EVENT_TIMEOUT_MS = 3000;
57
65
  const ANDROID_ORIENTATION_SAMPLE_TELEMETRY_MS = 3000;
@@ -1042,6 +1050,12 @@ export function VerifyAIScanner({
1042
1050
  const startupWatchdogRemountCount = startupWatchdogRemountCountRef.current + 1;
1043
1051
  startupWatchdogRemountCountRef.current = startupWatchdogRemountCount;
1044
1052
  cameraRemountCountRef.current++;
1053
+ // Escalate the backoff with each attempt (350, 700, 1050, capped at 1200ms) so a
1054
+ // camera that just needs more time isn't repeatedly interrupted at a fixed cadence.
1055
+ const startupRemountBackoffMs = Math.min(
1056
+ CAMERA_STARTUP_REMOUNT_BACKOFF_MS * startupWatchdogRemountCount,
1057
+ CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS,
1058
+ );
1045
1059
  telemetry?.track('camera_preview_timeout', {
1046
1060
  component: 'scanner',
1047
1061
  error: `Camera did not initialize within ${Math.round(watchdogMs / 1000)} seconds — remounting`,
@@ -1050,12 +1064,12 @@ export function VerifyAIScanner({
1050
1064
  remount_reason: 'startup_watchdog',
1051
1065
  startup_watchdog_remount_count: startupWatchdogRemountCount,
1052
1066
  startup_watchdog_max_remounts: CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS,
1053
- startup_remount_backoff_ms: CAMERA_STARTUP_REMOUNT_BACKOFF_MS,
1067
+ startup_remount_backoff_ms: startupRemountBackoffMs,
1054
1068
  torch_deferred_until_ready: enableTorch ? 1 : 0,
1055
1069
  }),
1056
1070
  });
1057
1071
  requestCameraRemount('startup_watchdog', {
1058
- backoffMs: CAMERA_STARTUP_REMOUNT_BACKOFF_MS,
1072
+ backoffMs: startupRemountBackoffMs,
1059
1073
  log: true,
1060
1074
  });
1061
1075
  }
@@ -1158,6 +1172,7 @@ export function VerifyAIScanner({
1158
1172
  let captureRetryTorchSuppressed = false;
1159
1173
  let captureRetrySettleDelayMs = CAMERA_CAPTURE_RETRY_DELAY_MS;
1160
1174
  let captureRetryRemountBackoffMs = 0;
1175
+ let captureRetryReadyTimeoutMs = 0;
1161
1176
  let lastNativeCaptureErrorMessage: string | null = null;
1162
1177
 
1163
1178
  try {
@@ -1237,13 +1252,11 @@ export function VerifyAIScanner({
1237
1252
 
1238
1253
  if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
1239
1254
  captureRetryAttempted = true;
1240
- captureRetryTorchSuppressed = !!enableTorch;
1241
- captureRetrySettleDelayMs = captureRetryTorchSuppressed
1242
- ? CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS
1243
- : CAMERA_CAPTURE_RETRY_DELAY_MS;
1244
- captureRetryRemountBackoffMs = captureRetryTorchSuppressed
1245
- ? CAMERA_CAPTURE_RETRY_DELAY_MS
1246
- : 0;
1255
+ const retryPlan = getCameraCaptureRetryPlan(Platform.OS, !!enableTorch);
1256
+ captureRetryTorchSuppressed = retryPlan.suppressTorch;
1257
+ captureRetrySettleDelayMs = retryPlan.settleDelayMs;
1258
+ captureRetryRemountBackoffMs = retryPlan.remountBackoffMs;
1259
+ captureRetryReadyTimeoutMs = retryPlan.readyTimeoutMs;
1247
1260
  const retryAt = new Date().toISOString();
1248
1261
  lastCaptureRetryAtRef.current = retryAt;
1249
1262
  cameraRemountCountRef.current++;
@@ -1257,9 +1270,6 @@ export function VerifyAIScanner({
1257
1270
  : undefined,
1258
1271
  );
1259
1272
  await sleep(captureRetrySettleDelayMs);
1260
- const captureRetryReadyTimeoutMs = Platform.OS === 'android'
1261
- ? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
1262
- : IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
1263
1273
  captureRetryReady = await waitForCameraReady(captureRetryReadyTimeoutMs);
1264
1274
  telemetry?.track('camera_capture_retry', {
1265
1275
  component: 'scanner',
@@ -1383,6 +1393,7 @@ export function VerifyAIScanner({
1383
1393
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
1384
1394
  capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
1385
1395
  capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
1396
+ capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
1386
1397
  recovered_native_capture_failure: captureRetryAttempted ? 1 : 0,
1387
1398
  last_native_capture_error: lastNativeCaptureErrorMessage,
1388
1399
  }),
@@ -1465,6 +1476,7 @@ export function VerifyAIScanner({
1465
1476
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
1466
1477
  capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
1467
1478
  capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
1479
+ capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
1468
1480
  is_native_camera_capture_error: isNativeCameraCaptureError(error),
1469
1481
  last_native_capture_error: lastNativeCaptureErrorMessage,
1470
1482
  }),
@@ -0,0 +1,37 @@
1
+ export const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
2
+ export const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
3
+ export const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
4
+ export const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
5
+
6
+ export type CameraCaptureRetryPlatform = 'ios' | 'android' | string;
7
+
8
+ export interface CameraCaptureRetryPlan {
9
+ readyTimeoutMs: number;
10
+ remountBackoffMs: number;
11
+ settleDelayMs: number;
12
+ suppressTorch: boolean;
13
+ }
14
+
15
+ export function getCameraCaptureRetryPlan(
16
+ platform: CameraCaptureRetryPlatform,
17
+ torchRequested: boolean,
18
+ ): CameraCaptureRetryPlan {
19
+ const suppressTorch = torchRequested;
20
+
21
+ return {
22
+ readyTimeoutMs: platform === 'android'
23
+ ? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
24
+ : IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS,
25
+ // Flutter fully restarts its CameraController after native capture errors.
26
+ // On iOS/Expo Camera, a key-only remount can report ready too quickly while
27
+ // the native capture session is still unable to produce a photo, so force a
28
+ // brief unmount before the retry.
29
+ remountBackoffMs: platform === 'ios' || suppressTorch
30
+ ? CAMERA_CAPTURE_RETRY_DELAY_MS
31
+ : 0,
32
+ settleDelayMs: suppressTorch
33
+ ? CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS
34
+ : CAMERA_CAPTURE_RETRY_DELAY_MS,
35
+ suppressTorch,
36
+ };
37
+ }
@@ -37,6 +37,8 @@ export interface VerificationResult {
37
37
  policy: string;
38
38
  violation_reasons: string[];
39
39
  feedback: string;
40
+ warning_violation_reasons?: string[];
41
+ warning_feedback?: string | null;
40
42
  metadata: Record<string, unknown>;
41
43
  image_url: string | null;
42
44
  /** Base64-encoded image data URI. Only present when `include_image_data` is requested. */
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.5.2';
1
+ export const SDK_VERSION = '2.5.4';