@switchlabs/verify-ai-react-native 2.5.6 → 2.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,32 @@
1
1
  # @switchlabs/verify-ai-react-native
2
2
 
3
- React Native SDK for Verify AI photo verification.
3
+ Send a photo and a policy, get back a pass/fail verdict with a confidence
4
+ value, the criteria that failed, and human-readable reasoning. Built for
5
+ end-of-ride parking compliance, vehicle and equipment condition and damage
6
+ grading, proof of delivery, and rental return checks.
7
+
8
+ The SDK wraps live camera capture (no gallery-picker path in the scanner
9
+ flow), on-device pre-screening, an offline queue, and the REST API.
10
+
11
+ ```tsx
12
+ const result = await verify({ image: base64, policy: 'scooter_parking' });
13
+ if (result && !result.is_compliant) {
14
+ console.log(result.violation_reasons, result.feedback);
15
+ }
16
+ ```
17
+
18
+ **Try it before you install anything:** upload a photo at
19
+ [verify.switchlabs.dev/demo](https://verify.switchlabs.dev/demo). No account,
20
+ no card.
21
+
22
+ **Get a key:** [free sandbox](https://verify.switchlabs.dev/login?mode=signup)
23
+ with $5 of credit and no credit card. Keys start with `vai_` and are sent in an
24
+ `X-API-Key` header.
25
+
26
+ **Pricing:** $0.008 per standard verification for the first 100,000 in a
27
+ calendar month, $0.006 from 100,001 to 1,000,000, and $0.005 above 1,000,000.
28
+ No platform fee, no monthly minimum, no annual contract.
29
+ [Full pricing](https://verify.switchlabs.dev/pricing).
4
30
 
5
31
  ## Install
6
32
 
@@ -2,6 +2,13 @@ import { TelemetryReporter } from '../telemetry/TelemetryReporter';
2
2
  import { SDK_VERSION } from '../version';
3
3
  const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
4
4
  const DEFAULT_TIMEOUT = 60000;
5
+ const MAX_IN_PROGRESS_RETRIES = 10;
6
+ function retryAfterMs(value) {
7
+ if (value !== null && /^\d+$/.test(value.trim()))
8
+ return Number(value) * 1000;
9
+ const date = value ? Date.parse(value) : NaN;
10
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 1000;
11
+ }
5
12
  const VEHICLE_TYPE_LABELS = {
6
13
  scooter: 'Scooter',
7
14
  'e-bike': 'E-Bike',
@@ -150,17 +157,34 @@ export class VerifyAIClient {
150
157
  const method = (options.method || 'GET').toUpperCase();
151
158
  const url = `${this.baseUrl}${path}`;
152
159
  const context = { path, url, method };
160
+ const deadline = Date.now() + this.timeout;
153
161
  try {
154
- const response = await fetch(url, {
155
- ...options,
156
- signal: controller.signal,
157
- headers: {
158
- 'X-API-Key': this.apiKey,
159
- ...options.headers,
160
- },
161
- });
162
- const rawBody = await response.text();
163
- const body = this.parseResponseBody(rawBody);
162
+ let response;
163
+ let body;
164
+ let retries = 0;
165
+ while (true) {
166
+ response = await fetch(url, {
167
+ ...options,
168
+ signal: controller.signal,
169
+ headers: {
170
+ 'X-API-Key': this.apiKey,
171
+ ...options.headers,
172
+ },
173
+ });
174
+ const rawBody = await response.text();
175
+ body = this.parseResponseBody(rawBody);
176
+ const code = body && typeof body === 'object' ? body.code : undefined;
177
+ const key = new Headers(options.headers).get('Idempotency-Key');
178
+ const delay = retryAfterMs(response.headers.get('Retry-After'));
179
+ // Only replay an identical, explicitly keyed verification reservation.
180
+ // Keep one timeout budget and report errors only after retries exhaust.
181
+ if (path !== '/verify' || method !== 'POST' || !key || response.status !== 409 ||
182
+ code !== 'idempotency_in_progress' || retries >= MAX_IN_PROGRESS_RETRIES ||
183
+ delay >= deadline - Date.now() || controller.signal.aborted)
184
+ break;
185
+ retries += 1;
186
+ await new Promise(resolve => setTimeout(resolve, delay));
187
+ }
164
188
  if (!response.ok) {
165
189
  const errorBody = (body && typeof body === 'object' ? body : null);
166
190
  throw this.buildRequestError(stringifyErrorMessage(errorBody?.error, `Request failed with status ${response.status}`), response.status, context, {
@@ -8,7 +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
+ import { CAMERA_CAPTURE_RETRY_DELAY_MS, getCameraCaptureRetryPlan, waitForCameraCaptureRetry, } from './cameraCaptureRetry';
12
12
  /** Quality used when expo-image-manipulator is not available (lower = smaller). */
13
13
  const FALLBACK_QUALITY = 0.65;
14
14
  /** Quality used when expo-image-manipulator IS available (resize handles size). */
@@ -79,9 +79,6 @@ function createScannerError(message, code, name) {
79
79
  error.code = code;
80
80
  return error;
81
81
  }
82
- function sleep(ms) {
83
- return new Promise((resolve) => setTimeout(resolve, ms));
84
- }
85
82
  function createScannerSessionId() {
86
83
  return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
87
84
  }
@@ -224,6 +221,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
224
221
  const terminalResultDeliveredRef = useRef(false);
225
222
  const appStateRef = useRef(AppState.currentState);
226
223
  const cameraReadyRef = useRef(false);
224
+ const cameraGenerationRef = useRef(0);
225
+ const scannerMountedRef = useRef(true);
226
+ const terminatedRef = useRef(false);
227
+ const renderedCameraGeneration = cameraGenerationRef.current;
227
228
  const cameraEverReadyRef = useRef(false);
228
229
  const cameraInitFailedRef = useRef(false);
229
230
  const permissionDeniedTrackedRef = useRef(false);
@@ -365,6 +366,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
365
366
  setTorchRetrySuppressed(nextSuppressed);
366
367
  }, []);
367
368
  useEffect(() => {
369
+ scannerMountedRef.current = true;
368
370
  const reporter = telemetryRef.current;
369
371
  if (reporter) {
370
372
  console.log(`VerifyAI[${SDK_VERSION}]: scanner telemetry attached ` +
@@ -385,6 +387,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
385
387
  }),
386
388
  });
387
389
  return () => {
390
+ scannerMountedRef.current = false;
391
+ cameraReadyRef.current = false;
392
+ if (startupRemountTimerRef.current)
393
+ clearTimeout(startupRemountTimerRef.current);
388
394
  const activeTelemetry = telemetryRef.current;
389
395
  activeTelemetry?.track('camera_scanner_disposed', {
390
396
  component: 'scanner',
@@ -401,6 +407,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
401
407
  }
402
408
  setCameraReady(false);
403
409
  cameraReadyRef.current = false;
410
+ const generation = ++cameraGenerationRef.current;
404
411
  cameraStartupAttemptStartedAtRef.current = Date.now();
405
412
  if (opts.log) {
406
413
  console.warn(`VerifyAI[${SDK_VERSION}]: remounting camera reason=${reason} ` +
@@ -408,6 +415,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
408
415
  `torchRetrySuppressed=${torchRetrySuppressedRef.current ? 1 : 0}`);
409
416
  }
410
417
  const remount = () => {
418
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' || cameraGenerationRef.current !== generation)
419
+ return;
411
420
  setCameraKey((k) => k + 1);
412
421
  setCameraMounted(true);
413
422
  startupRemountTimerRef.current = null;
@@ -688,6 +697,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
688
697
  }, []);
689
698
  // Release camera (and torch) when a terminal result is reached or on unmount.
690
699
  const releaseCamera = useCallback(() => {
700
+ terminatedRef.current = true;
701
+ cameraReadyRef.current = false;
702
+ cameraGenerationRef.current++;
691
703
  setTerminated(true);
692
704
  pausePreview();
693
705
  }, [pausePreview]);
@@ -735,7 +747,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
735
747
  onClose?.(terminalResultRef.current ?? result);
736
748
  }, [onClose, result]);
737
749
  // Camera init callbacks
738
- const onCameraReady = useCallback(() => {
750
+ const onCameraReady = useCallback((generation) => {
751
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' || generation !== cameraGenerationRef.current)
752
+ return;
739
753
  const readyAt = new Date().toISOString();
740
754
  const startupTotalElapsedMs = Date.now() - cameraStartupStartedAtRef.current;
741
755
  const startupAttemptElapsedMs = Date.now() - cameraStartupAttemptStartedAtRef.current;
@@ -968,6 +982,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
968
982
  setTimeout(() => setStatus('idle'), 2000);
969
983
  return;
970
984
  }
985
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active') {
986
+ captureIntentPendingRef.current = false;
987
+ return;
988
+ }
971
989
  setStatus('capturing');
972
990
  setTorchSuppressedForRetry(false);
973
991
  setResult(null);
@@ -978,7 +996,12 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
978
996
  clearTimeout(terminalResultTimerRef.current);
979
997
  terminalResultTimerRef.current = null;
980
998
  }
999
+ let captureCancelled = false;
981
1000
  let nativeCaptureAttempts = 0;
1001
+ let nativeCaptureElapsedMs = null;
1002
+ let nativeCaptureReadyAgeMs = null;
1003
+ let firstNativeCaptureElapsedMs = null;
1004
+ let firstNativeCaptureReadyAgeMs = null;
982
1005
  let captureRetryAttempted = false;
983
1006
  let captureRetryReady = false;
984
1007
  let captureRetryTorchSuppressed = false;
@@ -1025,39 +1048,59 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1025
1048
  catch {
1026
1049
  // Not installed — fall back to camera-only base64 below
1027
1050
  }
1028
- const waitForCameraReady = async (timeoutMs) => {
1029
- const start = Date.now();
1030
- while (Date.now() - start < timeoutMs) {
1031
- if (cameraReadyRef.current && cameraRef.current) {
1032
- return true;
1033
- }
1034
- await sleep(100);
1035
- }
1036
- return cameraReadyRef.current && !!cameraRef.current;
1037
- };
1038
1051
  const takePictureWithRetry = async (options, requiredField) => {
1039
1052
  let lastError = null;
1053
+ let preparedRetryCamera = null;
1054
+ let preparedRetryGeneration = null;
1040
1055
  for (let attempt = 1; attempt <= 2; attempt++) {
1056
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active') {
1057
+ captureCancelled = true;
1058
+ throw createScannerError('Camera capture cancelled', CAMERA_NOT_READY_ERROR_CODE, 'CameraCaptureCancelled');
1059
+ }
1060
+ const attemptCamera = cameraRef.current;
1061
+ if (!attemptCamera || !cameraReadyRef.current ||
1062
+ (attempt === 2 && (attemptCamera !== preparedRetryCamera || cameraGenerationRef.current !== preparedRetryGeneration))) {
1063
+ throw lastError ?? createScannerError('Camera is not ready for capture', CAMERA_NOT_READY_ERROR_CODE, 'CameraNotReady');
1064
+ }
1065
+ const attemptStartedAt = Date.now();
1066
+ const attemptGeneration = cameraGenerationRef.current;
1067
+ nativeCaptureReadyAgeMs = lastCameraReadyAtRef.current ? attemptStartedAt - Date.parse(lastCameraReadyAtRef.current) : null;
1041
1068
  nativeCaptureAttempts = attempt;
1042
1069
  try {
1043
- const photo = await cameraRef.current?.takePictureAsync(options);
1070
+ const photo = await attemptCamera?.takePictureAsync(options);
1071
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' ||
1072
+ cameraGenerationRef.current !== attemptGeneration || cameraRef.current !== attemptCamera) {
1073
+ captureCancelled = true;
1074
+ throw createScannerError('Camera capture cancelled', CAMERA_NOT_READY_ERROR_CODE, 'CameraCaptureCancelled');
1075
+ }
1076
+ nativeCaptureElapsedMs = Date.now() - attemptStartedAt;
1077
+ if (attempt === 1) {
1078
+ firstNativeCaptureElapsedMs = nativeCaptureElapsedMs;
1079
+ firstNativeCaptureReadyAgeMs = nativeCaptureReadyAgeMs;
1080
+ }
1044
1081
  if (!photo?.[requiredField]) {
1045
1082
  throw createScannerError('Failed to capture photo', CAMERA_CAPTURE_ERROR_CODE, 'CameraCaptureError');
1046
1083
  }
1047
1084
  return photo;
1048
1085
  }
1049
1086
  catch (captureErr) {
1087
+ nativeCaptureElapsedMs = Date.now() - attemptStartedAt;
1088
+ if (attempt === 1) {
1089
+ firstNativeCaptureElapsedMs = nativeCaptureElapsedMs;
1090
+ firstNativeCaptureReadyAgeMs = nativeCaptureReadyAgeMs;
1091
+ }
1050
1092
  const normalized = normalizeScannerError(captureErr);
1051
1093
  lastError = normalized;
1052
1094
  lastNativeCaptureErrorMessage = normalized.message;
1053
- if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
1095
+ if (attempt === 1 && isNativeCameraCaptureError(normalized) && scannerMountedRef.current && !terminatedRef.current && appStateRef.current === 'active') {
1054
1096
  captureRetryAttempted = true;
1055
1097
  const retryPlan = getCameraCaptureRetryPlan(Platform.OS, !!enableTorch);
1056
1098
  captureRetryTorchSuppressed = retryPlan.suppressTorch;
1057
1099
  captureRetrySettleDelayMs = retryPlan.settleDelayMs;
1058
1100
  captureRetryRemountBackoffMs = retryPlan.remountBackoffMs;
1059
1101
  captureRetryReadyTimeoutMs = retryPlan.readyTimeoutMs;
1060
- const retryAt = new Date().toISOString();
1102
+ const retryRequestedAt = Date.now();
1103
+ const retryAt = new Date(retryRequestedAt).toISOString();
1061
1104
  lastCaptureRetryAtRef.current = retryAt;
1062
1105
  cameraRemountCountRef.current++;
1063
1106
  if (captureRetryTorchSuppressed) {
@@ -1066,8 +1109,16 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1066
1109
  requestCameraRemount('capture_retry', captureRetryRemountBackoffMs > 0
1067
1110
  ? { backoffMs: captureRetryRemountBackoffMs }
1068
1111
  : undefined);
1069
- await sleep(captureRetrySettleDelayMs);
1070
- captureRetryReady = await waitForCameraReady(captureRetryReadyTimeoutMs);
1112
+ const retryGeneration = cameraGenerationRef.current;
1113
+ const retryCamera = await waitForCameraCaptureRetry(() => ({ camera: cameraRef.current, generation: cameraGenerationRef.current,
1114
+ ready: cameraReadyRef.current, active: scannerMountedRef.current && !terminatedRef.current && appStateRef.current === 'active' }), attemptCamera, retryGeneration, retryRequestedAt, retryPlan);
1115
+ captureRetryReady = !!retryCamera;
1116
+ preparedRetryCamera = retryCamera;
1117
+ preparedRetryGeneration = cameraGenerationRef.current;
1118
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' || cameraGenerationRef.current !== retryGeneration) {
1119
+ captureCancelled = true;
1120
+ throw normalized;
1121
+ }
1071
1122
  telemetry?.track('camera_capture_retry', {
1072
1123
  component: 'scanner',
1073
1124
  error: normalized,
@@ -1084,6 +1135,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1084
1135
  capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
1085
1136
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
1086
1137
  last_native_capture_error: lastNativeCaptureErrorMessage,
1138
+ first_native_capture_elapsed_ms: firstNativeCaptureElapsedMs,
1139
+ first_native_capture_ready_age_ms: firstNativeCaptureReadyAgeMs,
1087
1140
  }),
1088
1141
  });
1089
1142
  if (captureRetryReady) {
@@ -1159,6 +1212,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1159
1212
  resized: didResize ? 1 : 0,
1160
1213
  has_manipulator: ImageManipulator ? 1 : 0,
1161
1214
  native_capture_attempts: nativeCaptureAttempts,
1215
+ native_capture_elapsed_ms: nativeCaptureElapsedMs,
1216
+ native_capture_ready_age_ms: nativeCaptureReadyAgeMs,
1217
+ first_native_capture_elapsed_ms: firstNativeCaptureElapsedMs,
1218
+ first_native_capture_ready_age_ms: firstNativeCaptureReadyAgeMs,
1162
1219
  capture_retry_attempted: captureRetryAttempted,
1163
1220
  capture_retry_ready: captureRetryReady,
1164
1221
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
@@ -1213,6 +1270,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1213
1270
  }
1214
1271
  }
1215
1272
  catch (err) {
1273
+ if (captureCancelled || !scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active') {
1274
+ captureCancelled = true;
1275
+ return;
1276
+ }
1216
1277
  const error = normalizeScannerError(err);
1217
1278
  const terminalRequestError = isTerminalRequestError(error);
1218
1279
  if (terminalRequestError) {
@@ -1236,6 +1297,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1236
1297
  capture_attempt_id: captureAttemptId,
1237
1298
  capture_sequence: captureSequence,
1238
1299
  native_capture_attempts: nativeCaptureAttempts,
1300
+ native_capture_elapsed_ms: nativeCaptureElapsedMs,
1301
+ native_capture_ready_age_ms: nativeCaptureReadyAgeMs,
1302
+ first_native_capture_elapsed_ms: firstNativeCaptureElapsedMs,
1303
+ first_native_capture_ready_age_ms: firstNativeCaptureReadyAgeMs,
1239
1304
  capture_retry_attempted: captureRetryAttempted,
1240
1305
  capture_retry_ready: captureRetryReady,
1241
1306
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
@@ -1253,7 +1318,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1253
1318
  }
1254
1319
  finally {
1255
1320
  captureIntentPendingRef.current = false;
1256
- setTorchSuppressedForRetry(false);
1321
+ if (scannerMountedRef.current)
1322
+ setTorchSuppressedForRetry(false);
1323
+ if (captureCancelled && scannerMountedRef.current && !terminatedRef.current)
1324
+ setStatus('idle');
1257
1325
  }
1258
1326
  }, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg, enableTorch, setTorchSuppressedForRetry]);
1259
1327
  // Expose capture to parent via ref
@@ -1268,7 +1336,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
1268
1336
  }
1269
1337
  const showBottomCard = status === 'success' || status === 'error';
1270
1338
  const shouldEnableTorch = !terminated && cameraReady && !!enableTorch && !torchRetrySuppressed;
1271
- return (_jsx(View, { style: [styles.container, style], children: cameraMounted && (_jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: shouldEnableTorch, onCameraReady: onCameraReady, onMountError: onMountError, responsiveOrientationWhenOrientationLocked: true, onResponsiveOrientationChanged: (event) => {
1339
+ return (_jsx(View, { style: [styles.container, style], children: cameraMounted && (_jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: shouldEnableTorch, onCameraReady: () => onCameraReady(renderedCameraGeneration), onMountError: onMountError, responsiveOrientationWhenOrientationLocked: true, onResponsiveOrientationChanged: (event) => {
1272
1340
  setPhysicalOrientation(event.orientation);
1273
1341
  }, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: [styles.topBar, isLandscape && styles.topBarLandscape], children: _jsx(Text, { style: [
1274
1342
  styles.titleText,
@@ -1,4 +1,5 @@
1
1
  export declare const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
2
+ export declare const CAMERA_CAPTURE_RETRY_POST_READY_MS = 350;
2
3
  export declare const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
3
4
  export declare const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
4
5
  export declare const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
@@ -9,4 +10,12 @@ export interface CameraCaptureRetryPlan {
9
10
  settleDelayMs: number;
10
11
  suppressTorch: boolean;
11
12
  }
13
+ export interface CameraCaptureRetryState<T> {
14
+ camera: T | null;
15
+ generation: number;
16
+ ready: boolean;
17
+ active: boolean;
18
+ }
19
+ /** Wait for a fresh camera, preserving the existing total-delay/deadline policy. */
20
+ export declare function waitForCameraCaptureRetry<T>(readState: () => CameraCaptureRetryState<T>, previousCamera: T | null, generation: number, requestedAt: number, plan: CameraCaptureRetryPlan, sleep?: (ms: number) => Promise<void>, now?: () => number): Promise<T | null>;
12
21
  export declare function getCameraCaptureRetryPlan(platform: CameraCaptureRetryPlatform, torchRequested: boolean): CameraCaptureRetryPlan;
@@ -1,7 +1,28 @@
1
1
  export const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
2
+ export const CAMERA_CAPTURE_RETRY_POST_READY_MS = 350;
2
3
  export const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
3
4
  export const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
4
5
  export const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
6
+ /** Wait for a fresh camera, preserving the existing total-delay/deadline policy. */
7
+ export async function waitForCameraCaptureRetry(readState, previousCamera, generation, requestedAt, plan, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), now = Date.now) {
8
+ // Previously readiness polling began only after the total settle delay.
9
+ const deadline = requestedAt + plan.settleDelayMs + plan.readyTimeoutMs;
10
+ let state = readState();
11
+ while (state.active && state.generation === generation &&
12
+ !(state.ready && state.camera && state.camera !== previousCamera)) {
13
+ if (now() >= deadline)
14
+ return null;
15
+ await sleep(Math.min(100, deadline - now()));
16
+ state = readState();
17
+ }
18
+ if (!state.active || state.generation !== generation || !state.ready || !state.camera)
19
+ return null;
20
+ const camera = state.camera;
21
+ const captureAt = Math.max(requestedAt + plan.settleDelayMs, now() + CAMERA_CAPTURE_RETRY_POST_READY_MS);
22
+ await sleep(Math.max(0, captureAt - now()));
23
+ state = readState();
24
+ return state.active && state.generation === generation && state.ready && state.camera === camera ? camera : null;
25
+ }
5
26
  export function getCameraCaptureRetryPlan(platform, torchRequested) {
6
27
  const suppressTorch = torchRequested;
7
28
  return {
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.5.6";
1
+ export declare const SDK_VERSION = "2.5.8";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.5.6';
1
+ export const SDK_VERSION = '2.5.8';
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "2.5.6",
3
+ "version": "2.5.8",
4
4
  "description": "React Native SDK for Verify AI - photo verification with AI vision processing",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/levyelectric/verify.git",
8
- "directory": "packages/verify-ai-react-native"
5
+ "homepage": "https://verify.switchlabs.dev/docs/sdks/react-native",
6
+ "bugs": {
7
+ "url": "https://verify.switchlabs.dev/contact"
9
8
  },
10
9
  "main": "./lib/index.js",
11
10
  "types": "./lib/index.d.ts",
@@ -35,7 +34,12 @@
35
34
  "photo-verification",
36
35
  "ai-vision",
37
36
  "parking-compliance",
38
- "damage-detection"
37
+ "damage-detection",
38
+ "proof-of-delivery",
39
+ "image-verification",
40
+ "vehicle-inspection",
41
+ "expo",
42
+ "photo-verification-api"
39
43
  ],
40
44
  "author": "Switch Labs",
41
45
  "license": "MIT",
@@ -86,5 +90,10 @@
86
90
  "react-native-fast-tflite": "^1.6.1",
87
91
  "typescript": "^5.5.0",
88
92
  "vitest": "^4.1.0"
93
+ },
94
+ "repository": {
95
+ "type": "git",
96
+ "url": "git+https://github.com/levyelectric/verify.git",
97
+ "directory": "packages/verify-ai-react-native"
89
98
  }
90
99
  }
@@ -14,6 +14,13 @@ import { SDK_VERSION } from '../version';
14
14
 
15
15
  const DEFAULT_BASE_URL = 'https://verify.switchlabs.dev/api/v1';
16
16
  const DEFAULT_TIMEOUT = 60000;
17
+ const MAX_IN_PROGRESS_RETRIES = 10;
18
+
19
+ function retryAfterMs(value: string | null): number {
20
+ if (value !== null && /^\d+$/.test(value.trim())) return Number(value) * 1000;
21
+ const date = value ? Date.parse(value) : NaN;
22
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 1000;
23
+ }
17
24
 
18
25
  const VEHICLE_TYPE_LABELS: Record<string, string> = {
19
26
  scooter: 'Scooter',
@@ -214,19 +221,35 @@ export class VerifyAIClient {
214
221
  const method = (options.method || 'GET').toUpperCase();
215
222
  const url = `${this.baseUrl}${path}`;
216
223
  const context: RequestContext = { path, url, method };
224
+ const deadline = Date.now() + this.timeout;
217
225
 
218
226
  try {
219
- const response = await fetch(url, {
220
- ...options,
221
- signal: controller.signal,
222
- headers: {
223
- 'X-API-Key': this.apiKey,
224
- ...options.headers,
225
- },
226
- });
227
-
228
- const rawBody = await response.text();
229
- const body = this.parseResponseBody(rawBody);
227
+ let response: Response;
228
+ let body: unknown;
229
+ let retries = 0;
230
+ while (true) {
231
+ response = await fetch(url, {
232
+ ...options,
233
+ signal: controller.signal,
234
+ headers: {
235
+ 'X-API-Key': this.apiKey,
236
+ ...options.headers,
237
+ },
238
+ });
239
+
240
+ const rawBody = await response.text();
241
+ body = this.parseResponseBody(rawBody);
242
+ const code = body && typeof body === 'object' ? (body as VerifyAIError).code : undefined;
243
+ const key = new Headers(options.headers).get('Idempotency-Key');
244
+ const delay = retryAfterMs(response.headers.get('Retry-After'));
245
+ // Only replay an identical, explicitly keyed verification reservation.
246
+ // Keep one timeout budget and report errors only after retries exhaust.
247
+ if (path !== '/verify' || method !== 'POST' || !key || response.status !== 409 ||
248
+ code !== 'idempotency_in_progress' || retries >= MAX_IN_PROGRESS_RETRIES ||
249
+ delay >= deadline - Date.now() || controller.signal.aborted) break;
250
+ retries += 1;
251
+ await new Promise(resolve => setTimeout(resolve, delay));
252
+ }
230
253
 
231
254
  if (!response.ok) {
232
255
  const errorBody = (body && typeof body === 'object' ? body : null) as VerifyAIError | null;
@@ -35,6 +35,7 @@ import {
35
35
  import {
36
36
  CAMERA_CAPTURE_RETRY_DELAY_MS,
37
37
  getCameraCaptureRetryPlan,
38
+ waitForCameraCaptureRetry,
38
39
  } from './cameraCaptureRetry';
39
40
 
40
41
  /** Quality used when expo-image-manipulator is not available (lower = smaller). */
@@ -158,10 +159,6 @@ function createScannerError(message: string, code: string, name: string): ErrorW
158
159
  return error;
159
160
  }
160
161
 
161
- function sleep(ms: number): Promise<void> {
162
- return new Promise((resolve) => setTimeout(resolve, ms));
163
- }
164
-
165
162
  function createScannerSessionId(): string {
166
163
  return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
167
164
  }
@@ -330,6 +327,10 @@ export function VerifyAIScanner({
330
327
  const terminalResultDeliveredRef = useRef(false);
331
328
  const appStateRef = useRef(AppState.currentState);
332
329
  const cameraReadyRef = useRef(false);
330
+ const cameraGenerationRef = useRef(0);
331
+ const scannerMountedRef = useRef(true);
332
+ const terminatedRef = useRef(false);
333
+ const renderedCameraGeneration = cameraGenerationRef.current;
333
334
  const cameraEverReadyRef = useRef(false);
334
335
  const cameraInitFailedRef = useRef(false);
335
336
  const permissionDeniedTrackedRef = useRef(false);
@@ -483,6 +484,7 @@ export function VerifyAIScanner({
483
484
  }, []);
484
485
 
485
486
  useEffect(() => {
487
+ scannerMountedRef.current = true;
486
488
  const reporter = telemetryRef.current;
487
489
  if (reporter) {
488
490
  console.log(
@@ -507,6 +509,9 @@ export function VerifyAIScanner({
507
509
  });
508
510
 
509
511
  return () => {
512
+ scannerMountedRef.current = false;
513
+ cameraReadyRef.current = false;
514
+ if (startupRemountTimerRef.current) clearTimeout(startupRemountTimerRef.current);
510
515
  const activeTelemetry = telemetryRef.current;
511
516
  activeTelemetry?.track('camera_scanner_disposed', {
512
517
  component: 'scanner',
@@ -531,6 +536,7 @@ export function VerifyAIScanner({
531
536
 
532
537
  setCameraReady(false);
533
538
  cameraReadyRef.current = false;
539
+ const generation = ++cameraGenerationRef.current;
534
540
  cameraStartupAttemptStartedAtRef.current = Date.now();
535
541
 
536
542
  if (opts.log) {
@@ -542,6 +548,7 @@ export function VerifyAIScanner({
542
548
  }
543
549
 
544
550
  const remount = () => {
551
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' || cameraGenerationRef.current !== generation) return;
545
552
  setCameraKey((k) => k + 1);
546
553
  setCameraMounted(true);
547
554
  startupRemountTimerRef.current = null;
@@ -868,6 +875,9 @@ export function VerifyAIScanner({
868
875
 
869
876
  // Release camera (and torch) when a terminal result is reached or on unmount.
870
877
  const releaseCamera = useCallback(() => {
878
+ terminatedRef.current = true;
879
+ cameraReadyRef.current = false;
880
+ cameraGenerationRef.current++;
871
881
  setTerminated(true);
872
882
  pausePreview();
873
883
  }, [pausePreview]);
@@ -920,7 +930,8 @@ export function VerifyAIScanner({
920
930
  }, [onClose, result]);
921
931
 
922
932
  // Camera init callbacks
923
- const onCameraReady = useCallback(() => {
933
+ const onCameraReady = useCallback((generation: number) => {
934
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' || generation !== cameraGenerationRef.current) return;
924
935
  const readyAt = new Date().toISOString();
925
936
  const startupTotalElapsedMs = Date.now() - cameraStartupStartedAtRef.current;
926
937
  const startupAttemptElapsedMs = Date.now() - cameraStartupAttemptStartedAtRef.current;
@@ -1184,6 +1195,10 @@ export function VerifyAIScanner({
1184
1195
  return;
1185
1196
  }
1186
1197
 
1198
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active') {
1199
+ captureIntentPendingRef.current = false;
1200
+ return;
1201
+ }
1187
1202
  setStatus('capturing');
1188
1203
  setTorchSuppressedForRetry(false);
1189
1204
  setResult(null);
@@ -1195,7 +1210,12 @@ export function VerifyAIScanner({
1195
1210
  terminalResultTimerRef.current = null;
1196
1211
  }
1197
1212
 
1213
+ let captureCancelled = false;
1198
1214
  let nativeCaptureAttempts = 0;
1215
+ let nativeCaptureElapsedMs: number | null = null;
1216
+ let nativeCaptureReadyAgeMs: number | null = null;
1217
+ let firstNativeCaptureElapsedMs: number | null = null;
1218
+ let firstNativeCaptureReadyAgeMs: number | null = null;
1199
1219
  let captureRetryAttempted = false;
1200
1220
  let captureRetryReady = false;
1201
1221
  let captureRetryTorchSuppressed = false;
@@ -1245,27 +1265,40 @@ export function VerifyAIScanner({
1245
1265
  // Not installed — fall back to camera-only base64 below
1246
1266
  }
1247
1267
 
1248
- const waitForCameraReady = async (timeoutMs: number): Promise<boolean> => {
1249
- const start = Date.now();
1250
- while (Date.now() - start < timeoutMs) {
1251
- if (cameraReadyRef.current && cameraRef.current) {
1252
- return true;
1253
- }
1254
- await sleep(100);
1255
- }
1256
- return cameraReadyRef.current && !!cameraRef.current;
1257
- };
1258
-
1259
1268
  const takePictureWithRetry = async (
1260
1269
  options: { base64?: boolean; quality?: number; exif?: boolean },
1261
1270
  requiredField: 'uri' | 'base64',
1262
1271
  ) => {
1263
1272
  let lastError: ErrorWithDetails | null = null;
1273
+ let preparedRetryCamera: CameraView | null = null;
1274
+ let preparedRetryGeneration: number | null = null;
1264
1275
 
1265
1276
  for (let attempt = 1; attempt <= 2; attempt++) {
1277
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active') {
1278
+ captureCancelled = true;
1279
+ throw createScannerError('Camera capture cancelled', CAMERA_NOT_READY_ERROR_CODE, 'CameraCaptureCancelled');
1280
+ }
1281
+ const attemptCamera = cameraRef.current;
1282
+ if (!attemptCamera || !cameraReadyRef.current ||
1283
+ (attempt === 2 && (attemptCamera !== preparedRetryCamera || cameraGenerationRef.current !== preparedRetryGeneration))) {
1284
+ throw lastError ?? createScannerError('Camera is not ready for capture', CAMERA_NOT_READY_ERROR_CODE, 'CameraNotReady');
1285
+ }
1286
+ const attemptStartedAt = Date.now();
1287
+ const attemptGeneration = cameraGenerationRef.current;
1288
+ nativeCaptureReadyAgeMs = lastCameraReadyAtRef.current ? attemptStartedAt - Date.parse(lastCameraReadyAtRef.current) : null;
1266
1289
  nativeCaptureAttempts = attempt;
1267
1290
  try {
1268
- const photo = await cameraRef.current?.takePictureAsync(options);
1291
+ const photo = await attemptCamera?.takePictureAsync(options);
1292
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' ||
1293
+ cameraGenerationRef.current !== attemptGeneration || cameraRef.current !== attemptCamera) {
1294
+ captureCancelled = true;
1295
+ throw createScannerError('Camera capture cancelled', CAMERA_NOT_READY_ERROR_CODE, 'CameraCaptureCancelled');
1296
+ }
1297
+ nativeCaptureElapsedMs = Date.now() - attemptStartedAt;
1298
+ if (attempt === 1) {
1299
+ firstNativeCaptureElapsedMs = nativeCaptureElapsedMs;
1300
+ firstNativeCaptureReadyAgeMs = nativeCaptureReadyAgeMs;
1301
+ }
1269
1302
  if (!photo?.[requiredField]) {
1270
1303
  throw createScannerError(
1271
1304
  'Failed to capture photo',
@@ -1275,18 +1308,24 @@ export function VerifyAIScanner({
1275
1308
  }
1276
1309
  return photo;
1277
1310
  } catch (captureErr) {
1311
+ nativeCaptureElapsedMs = Date.now() - attemptStartedAt;
1312
+ if (attempt === 1) {
1313
+ firstNativeCaptureElapsedMs = nativeCaptureElapsedMs;
1314
+ firstNativeCaptureReadyAgeMs = nativeCaptureReadyAgeMs;
1315
+ }
1278
1316
  const normalized = normalizeScannerError(captureErr);
1279
1317
  lastError = normalized;
1280
1318
  lastNativeCaptureErrorMessage = normalized.message;
1281
1319
 
1282
- if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
1320
+ if (attempt === 1 && isNativeCameraCaptureError(normalized) && scannerMountedRef.current && !terminatedRef.current && appStateRef.current === 'active') {
1283
1321
  captureRetryAttempted = true;
1284
1322
  const retryPlan = getCameraCaptureRetryPlan(Platform.OS, !!enableTorch);
1285
1323
  captureRetryTorchSuppressed = retryPlan.suppressTorch;
1286
1324
  captureRetrySettleDelayMs = retryPlan.settleDelayMs;
1287
1325
  captureRetryRemountBackoffMs = retryPlan.remountBackoffMs;
1288
1326
  captureRetryReadyTimeoutMs = retryPlan.readyTimeoutMs;
1289
- const retryAt = new Date().toISOString();
1327
+ const retryRequestedAt = Date.now();
1328
+ const retryAt = new Date(retryRequestedAt).toISOString();
1290
1329
  lastCaptureRetryAtRef.current = retryAt;
1291
1330
  cameraRemountCountRef.current++;
1292
1331
  if (captureRetryTorchSuppressed) {
@@ -1298,8 +1337,19 @@ export function VerifyAIScanner({
1298
1337
  ? { backoffMs: captureRetryRemountBackoffMs }
1299
1338
  : undefined,
1300
1339
  );
1301
- await sleep(captureRetrySettleDelayMs);
1302
- captureRetryReady = await waitForCameraReady(captureRetryReadyTimeoutMs);
1340
+ const retryGeneration = cameraGenerationRef.current;
1341
+ const retryCamera: CameraView | null = await waitForCameraCaptureRetry<CameraView>(
1342
+ () => ({camera: cameraRef.current, generation: cameraGenerationRef.current,
1343
+ ready: cameraReadyRef.current, active: scannerMountedRef.current && !terminatedRef.current && appStateRef.current === 'active'}),
1344
+ attemptCamera, retryGeneration, retryRequestedAt, retryPlan,
1345
+ );
1346
+ captureRetryReady = !!retryCamera;
1347
+ preparedRetryCamera = retryCamera;
1348
+ preparedRetryGeneration = cameraGenerationRef.current;
1349
+ if (!scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active' || cameraGenerationRef.current !== retryGeneration) {
1350
+ captureCancelled = true;
1351
+ throw normalized;
1352
+ }
1303
1353
  telemetry?.track('camera_capture_retry', {
1304
1354
  component: 'scanner',
1305
1355
  error: normalized,
@@ -1316,6 +1366,8 @@ export function VerifyAIScanner({
1316
1366
  capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
1317
1367
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
1318
1368
  last_native_capture_error: lastNativeCaptureErrorMessage,
1369
+ first_native_capture_elapsed_ms: firstNativeCaptureElapsedMs,
1370
+ first_native_capture_ready_age_ms: firstNativeCaptureReadyAgeMs,
1319
1371
  }),
1320
1372
  });
1321
1373
  if (captureRetryReady) {
@@ -1417,6 +1469,10 @@ export function VerifyAIScanner({
1417
1469
  resized: didResize ? 1 : 0,
1418
1470
  has_manipulator: ImageManipulator ? 1 : 0,
1419
1471
  native_capture_attempts: nativeCaptureAttempts,
1472
+ native_capture_elapsed_ms: nativeCaptureElapsedMs,
1473
+ native_capture_ready_age_ms: nativeCaptureReadyAgeMs,
1474
+ first_native_capture_elapsed_ms: firstNativeCaptureElapsedMs,
1475
+ first_native_capture_ready_age_ms: firstNativeCaptureReadyAgeMs,
1420
1476
  capture_retry_attempted: captureRetryAttempted,
1421
1477
  capture_retry_ready: captureRetryReady,
1422
1478
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
@@ -1474,6 +1530,10 @@ export function VerifyAIScanner({
1474
1530
  setStatus('idle');
1475
1531
  }
1476
1532
  } catch (err) {
1533
+ if (captureCancelled || !scannerMountedRef.current || terminatedRef.current || appStateRef.current !== 'active') {
1534
+ captureCancelled = true;
1535
+ return;
1536
+ }
1477
1537
  const error = normalizeScannerError(err);
1478
1538
  const terminalRequestError = isTerminalRequestError(error);
1479
1539
  if (terminalRequestError) {
@@ -1500,6 +1560,10 @@ export function VerifyAIScanner({
1500
1560
  capture_attempt_id: captureAttemptId,
1501
1561
  capture_sequence: captureSequence,
1502
1562
  native_capture_attempts: nativeCaptureAttempts,
1563
+ native_capture_elapsed_ms: nativeCaptureElapsedMs,
1564
+ native_capture_ready_age_ms: nativeCaptureReadyAgeMs,
1565
+ first_native_capture_elapsed_ms: firstNativeCaptureElapsedMs,
1566
+ first_native_capture_ready_age_ms: firstNativeCaptureReadyAgeMs,
1503
1567
  capture_retry_attempted: captureRetryAttempted,
1504
1568
  capture_retry_ready: captureRetryReady,
1505
1569
  capture_retry_torch_suppressed: captureRetryTorchSuppressed,
@@ -1518,7 +1582,8 @@ export function VerifyAIScanner({
1518
1582
  }
1519
1583
  } finally {
1520
1584
  captureIntentPendingRef.current = false;
1521
- setTorchSuppressedForRetry(false);
1585
+ if (scannerMountedRef.current) setTorchSuppressedForRetry(false);
1586
+ if (captureCancelled && scannerMountedRef.current && !terminatedRef.current) setStatus('idle');
1522
1587
  }
1523
1588
  }, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg, enableTorch, setTorchSuppressedForRetry]);
1524
1589
 
@@ -1554,7 +1619,7 @@ export function VerifyAIScanner({
1554
1619
  style={styles.camera}
1555
1620
  facing="back"
1556
1621
  enableTorch={shouldEnableTorch}
1557
- onCameraReady={onCameraReady}
1622
+ onCameraReady={() => onCameraReady(renderedCameraGeneration)}
1558
1623
  onMountError={onMountError}
1559
1624
  responsiveOrientationWhenOrientationLocked
1560
1625
  onResponsiveOrientationChanged={(event) => {
@@ -1,4 +1,5 @@
1
1
  export const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
2
+ export const CAMERA_CAPTURE_RETRY_POST_READY_MS = 350;
2
3
  export const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
3
4
  export const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
4
5
  export const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
@@ -12,6 +13,40 @@ export interface CameraCaptureRetryPlan {
12
13
  suppressTorch: boolean;
13
14
  }
14
15
 
16
+ export interface CameraCaptureRetryState<T> {
17
+ camera: T | null;
18
+ generation: number;
19
+ ready: boolean;
20
+ active: boolean;
21
+ }
22
+
23
+ /** Wait for a fresh camera, preserving the existing total-delay/deadline policy. */
24
+ export async function waitForCameraCaptureRetry<T>(
25
+ readState: () => CameraCaptureRetryState<T>,
26
+ previousCamera: T | null,
27
+ generation: number,
28
+ requestedAt: number,
29
+ plan: CameraCaptureRetryPlan,
30
+ sleep: (ms: number) => Promise<void> = ms => new Promise(resolve => setTimeout(resolve, ms)),
31
+ now: () => number = Date.now,
32
+ ): Promise<T | null> {
33
+ // Previously readiness polling began only after the total settle delay.
34
+ const deadline = requestedAt + plan.settleDelayMs + plan.readyTimeoutMs;
35
+ let state = readState();
36
+ while (state.active && state.generation === generation &&
37
+ !(state.ready && state.camera && state.camera !== previousCamera)) {
38
+ if (now() >= deadline) return null;
39
+ await sleep(Math.min(100, deadline - now()));
40
+ state = readState();
41
+ }
42
+ if (!state.active || state.generation !== generation || !state.ready || !state.camera) return null;
43
+ const camera = state.camera;
44
+ const captureAt = Math.max(requestedAt + plan.settleDelayMs, now() + CAMERA_CAPTURE_RETRY_POST_READY_MS);
45
+ await sleep(Math.max(0, captureAt - now()));
46
+ state = readState();
47
+ return state.active && state.generation === generation && state.ready && state.camera === camera ? camera : null;
48
+ }
49
+
15
50
  export function getCameraCaptureRetryPlan(
16
51
  platform: CameraCaptureRetryPlatform,
17
52
  torchRequested: boolean,
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.5.6';
1
+ export const SDK_VERSION = '2.5.8';