@switchlabs/verify-ai-react-native 2.1.0 → 2.3.1

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.
@@ -195,11 +195,37 @@ export class VerifyAIClient {
195
195
  if (options?.idempotencyKey) {
196
196
  headers['Idempotency-Key'] = options.idempotencyKey;
197
197
  }
198
- return this.executeRequest('/verify', {
199
- method: 'POST',
200
- headers,
201
- body: formData,
202
- });
198
+ try {
199
+ return await this.executeRequest('/verify', {
200
+ method: 'POST',
201
+ headers,
202
+ body: formData,
203
+ });
204
+ }
205
+ catch (error) {
206
+ // Blank 500 means the server crashed during multipart parsing (e.g.
207
+ // Vercel function crash). Retry as JSON/base64 with a fresh request
208
+ // — the crashed request may have partially recorded the idempotency key.
209
+ if (error instanceof VerifyAIRequestError && error.status >= 500 && error.isServerError) {
210
+ try {
211
+ const { Buffer } = await import('buffer');
212
+ const fileResponse = await fetch(request.imageUri);
213
+ const arrayBuffer = await fileResponse.arrayBuffer();
214
+ const base64 = Buffer.from(arrayBuffer).toString('base64');
215
+ return this.verify({
216
+ image: base64,
217
+ policy: request.policy,
218
+ metadata: request.metadata,
219
+ provider: request.provider,
220
+ });
221
+ }
222
+ catch {
223
+ // If the retry itself fails, throw the original error
224
+ throw error;
225
+ }
226
+ }
227
+ throw error;
228
+ }
203
229
  }
204
230
  /**
205
231
  * List past verifications with optional filters.
@@ -312,12 +312,12 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
312
312
  return (_jsxs(View, { style: [styles.container, styles.permissionContainer, style], children: [_jsx(Text, { style: styles.permissionText, children: "Camera access is required for photo verification" }), _jsx(TouchableOpacity, { style: styles.permissionButton, onPress: requestPermission, children: _jsx(Text, { style: styles.permissionButtonText, children: "Grant Camera Access" }) })] }));
313
313
  }
314
314
  const showBottomCard = status === 'success' || status === 'error';
315
- return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: !terminated && enableTorch, onCameraReady: onCameraReady, onMountError: onMountError, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: styles.topBar, children: _jsx(Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && (_jsx(View, { style: styles.guideContainer, children: _jsxs(View, { style: [
316
- styles.guideFrame,
317
- overlay.guideFrameAspectRatio
318
- ? { aspectRatio: overlay.guideFrameAspectRatio }
319
- : 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] })] }) })), 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: [
315
+ return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: !terminated && enableTorch, onCameraReady: onCameraReady, onMountError: onMountError, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: styles.topBar, children: _jsx(Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && (_jsxs(View, { style: styles.guideContainer, children: [_jsxs(View, { style: [
316
+ styles.guideFrame,
317
+ overlay.guideFrameAspectRatio
318
+ ? { aspectRatio: overlay.guideFrameAspectRatio }
319
+ : 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: [
321
321
  styles.resultIconCircle,
322
322
  result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
323
323
  ], children: _jsx(Text, { style: styles.resultIcon, children: result.is_compliant ? '\u2713' : '\u2717' }) }), _jsx(Text, { style: [
@@ -334,9 +334,16 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
334
334
  }
335
335
  else if (overlay?.maxAttempts != null && attemptCountRef.current < overlay.maxAttempts && result && !result.is_compliant) {
336
336
  const remaining = overlay.maxAttempts - attemptCountRef.current;
337
- errorTitle = 'Not Verified';
338
- const template = overlay?.retryMessage || 'Please try again. {remaining} attempts left.';
339
- errorMessage = template.replace('{remaining}', String(remaining));
337
+ errorTitle = overlay?.failureMessage || 'Not Verified';
338
+ if (overlay?.retryMessage) {
339
+ errorMessage = overlay.retryMessage.replace('{remaining}', String(remaining));
340
+ }
341
+ else {
342
+ // Show actual API feedback with retry count (matches Flutter)
343
+ const feedback = result.feedback?.trim();
344
+ const retryInfo = `${remaining} attempt${remaining === 1 ? '' : 's'} remaining.`;
345
+ errorMessage = feedback ? `${feedback}\n\n${retryInfo}` : `Please try again. ${retryInfo}`;
346
+ }
340
347
  }
341
348
  else {
342
349
  const display = getErrorDisplay(lastError, overlay?.showTechnicalErrorDetails);
@@ -382,6 +389,14 @@ const styles = StyleSheet.create({
382
389
  alignItems: 'center',
383
390
  paddingHorizontal: 40,
384
391
  },
392
+ guideCaptionText: {
393
+ color: 'rgba(255,255,255,0.8)',
394
+ fontSize: 13,
395
+ fontWeight: '500',
396
+ textAlign: 'center',
397
+ marginTop: 12,
398
+ lineHeight: 18,
399
+ },
385
400
  guideFrame: {
386
401
  width: '100%',
387
402
  aspectRatio: 4 / 3,
@@ -30,6 +30,8 @@ export interface VerificationResult {
30
30
  feedback: string;
31
31
  metadata: Record<string, unknown>;
32
32
  image_url: string | null;
33
+ /** Classification category (e.g. good_parking, no_vehicle, poor_photo). */
34
+ category?: string;
33
35
  }
34
36
  export interface VerificationListResponse {
35
37
  data: VerificationResult[];
@@ -87,6 +89,8 @@ export interface ScannerOverlayConfig {
87
89
  guideOverlayContent?: React.ReactNode;
88
90
  /** Opacity of the guideOverlayContent (0–1). Default: 0.3. */
89
91
  guideOverlayOpacity?: number;
92
+ /** Caption text shown directly below the guide frame. */
93
+ guideCaption?: string;
90
94
  processingMessage?: string;
91
95
  successMessage?: string;
92
96
  failureMessage?: string;
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.1.0";
1
+ export declare const SDK_VERSION = "2.3.1";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.1.0';
1
+ export const SDK_VERSION = '2.3.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "2.1.0",
3
+ "version": "2.3.1",
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",
@@ -262,11 +262,35 @@ export class VerifyAIClient {
262
262
  headers['Idempotency-Key'] = options.idempotencyKey;
263
263
  }
264
264
 
265
- return this.executeRequest<VerificationResult>('/verify', {
266
- method: 'POST',
267
- headers,
268
- body: formData,
269
- });
265
+ try {
266
+ return await this.executeRequest<VerificationResult>('/verify', {
267
+ method: 'POST',
268
+ headers,
269
+ body: formData,
270
+ });
271
+ } catch (error) {
272
+ // Blank 500 means the server crashed during multipart parsing (e.g.
273
+ // Vercel function crash). Retry as JSON/base64 with a fresh request
274
+ // — the crashed request may have partially recorded the idempotency key.
275
+ if (error instanceof VerifyAIRequestError && error.status >= 500 && error.isServerError) {
276
+ try {
277
+ const { Buffer } = await import('buffer');
278
+ const fileResponse = await fetch(request.imageUri);
279
+ const arrayBuffer = await fileResponse.arrayBuffer();
280
+ const base64 = Buffer.from(arrayBuffer).toString('base64');
281
+ return this.verify({
282
+ image: base64,
283
+ policy: request.policy,
284
+ metadata: request.metadata,
285
+ provider: request.provider,
286
+ });
287
+ } catch {
288
+ // If the retry itself fails, throw the original error
289
+ throw error;
290
+ }
291
+ }
292
+ throw error;
293
+ }
270
294
  }
271
295
 
272
296
  /**
@@ -450,6 +450,9 @@ export function VerifyAIScanner({
450
450
  <View style={[styles.corner, styles.cornerBottomLeft]} />
451
451
  <View style={[styles.corner, styles.cornerBottomRight]} />
452
452
  </View>
453
+ {overlay.guideCaption && (
454
+ <Text style={styles.guideCaptionText}>{overlay.guideCaption}</Text>
455
+ )}
453
456
  </View>
454
457
  )}
455
458
 
@@ -505,9 +508,15 @@ export function VerifyAIScanner({
505
508
  errorMessage = overlay?.exhaustedMessage || 'Maximum attempts reached.';
506
509
  } else if (overlay?.maxAttempts != null && attemptCountRef.current < overlay.maxAttempts && result && !result.is_compliant) {
507
510
  const remaining = overlay.maxAttempts - attemptCountRef.current;
508
- errorTitle = 'Not Verified';
509
- const template = overlay?.retryMessage || 'Please try again. {remaining} attempts left.';
510
- errorMessage = template.replace('{remaining}', String(remaining));
511
+ errorTitle = overlay?.failureMessage || 'Not Verified';
512
+ if (overlay?.retryMessage) {
513
+ errorMessage = overlay.retryMessage.replace('{remaining}', String(remaining));
514
+ } else {
515
+ // Show actual API feedback with retry count (matches Flutter)
516
+ const feedback = result.feedback?.trim();
517
+ const retryInfo = `${remaining} attempt${remaining === 1 ? '' : 's'} remaining.`;
518
+ errorMessage = feedback ? `${feedback}\n\n${retryInfo}` : `Please try again. ${retryInfo}`;
519
+ }
511
520
  } else {
512
521
  const display = getErrorDisplay(lastError, overlay?.showTechnicalErrorDetails);
513
522
  errorTitle = display.title;
@@ -595,6 +604,14 @@ const styles = StyleSheet.create({
595
604
  alignItems: 'center',
596
605
  paddingHorizontal: 40,
597
606
  },
607
+ guideCaptionText: {
608
+ color: 'rgba(255,255,255,0.8)',
609
+ fontSize: 13,
610
+ fontWeight: '500',
611
+ textAlign: 'center' as const,
612
+ marginTop: 12,
613
+ lineHeight: 18,
614
+ },
598
615
  guideFrame: {
599
616
  width: '100%',
600
617
  aspectRatio: 4 / 3,
@@ -34,6 +34,8 @@ export interface VerificationResult {
34
34
  feedback: string;
35
35
  metadata: Record<string, unknown>;
36
36
  image_url: string | null;
37
+ /** Classification category (e.g. good_parking, no_vehicle, poor_photo). */
38
+ category?: string;
37
39
  }
38
40
 
39
41
  export interface VerificationListResponse {
@@ -98,6 +100,8 @@ export interface ScannerOverlayConfig {
98
100
  guideOverlayContent?: React.ReactNode;
99
101
  /** Opacity of the guideOverlayContent (0–1). Default: 0.3. */
100
102
  guideOverlayOpacity?: number;
103
+ /** Caption text shown directly below the guide frame. */
104
+ guideCaption?: string;
101
105
  processingMessage?: string;
102
106
  successMessage?: string;
103
107
  failureMessage?: string;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.1.0';
1
+ export const SDK_VERSION = '2.3.1';