@switchlabs/verify-ai-react-native 2.4.21 → 2.4.23

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.
@@ -93,7 +93,7 @@ export declare class VerifyAIClient {
93
93
  export declare class VerifyAIRequestError extends Error {
94
94
  status: number;
95
95
  body: VerifyAIError;
96
- constructor(message: string, status: number, body: VerifyAIError);
96
+ constructor(message: unknown, status: number, body: VerifyAIError);
97
97
  get isRateLimited(): boolean;
98
98
  get isUnauthorized(): boolean;
99
99
  get isServerError(): boolean;
@@ -19,6 +19,39 @@ function enrichMetadata(metadata) {
19
19
  }
20
20
  return enriched;
21
21
  }
22
+ function stringifyErrorMessage(value, fallback) {
23
+ if (typeof value === 'string' && value.trim()) {
24
+ return value;
25
+ }
26
+ if (value instanceof Error && value.message) {
27
+ return value.message;
28
+ }
29
+ if (value && typeof value === 'object') {
30
+ const record = value;
31
+ for (const key of ['message', 'error', 'detail', 'code']) {
32
+ const candidate = record[key];
33
+ if (typeof candidate === 'string' && candidate.trim()) {
34
+ return candidate;
35
+ }
36
+ }
37
+ try {
38
+ const json = JSON.stringify(value);
39
+ if (json && json !== '{}') {
40
+ return json;
41
+ }
42
+ }
43
+ catch {
44
+ // Fall through to String(value) below.
45
+ }
46
+ }
47
+ if (value != null) {
48
+ const text = String(value);
49
+ if (text && text !== '[object Object]') {
50
+ return text;
51
+ }
52
+ }
53
+ return fallback;
54
+ }
22
55
  export class VerifyAIClient {
23
56
  constructor(config) {
24
57
  if (!config.apiKey) {
@@ -43,8 +76,9 @@ export class VerifyAIClient {
43
76
  }
44
77
  }
45
78
  buildRequestError(message, status, context, body = {}) {
79
+ const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
46
80
  const error = {
47
- error: message,
81
+ error: safeMessage,
48
82
  status,
49
83
  code: status === 408 ? 'timeout' : status === 0 ? 'network_error' : 'request_error',
50
84
  path: context.path,
@@ -52,7 +86,7 @@ export class VerifyAIClient {
52
86
  method: context.method,
53
87
  ...body,
54
88
  };
55
- return new VerifyAIRequestError(message, status, error);
89
+ return new VerifyAIRequestError(safeMessage, status, error);
56
90
  }
57
91
  normalizeRequestError(error, context) {
58
92
  if (error instanceof VerifyAIRequestError) {
@@ -64,7 +98,7 @@ export class VerifyAIClient {
64
98
  if (error instanceof TypeError) {
65
99
  return this.buildRequestError('Network request failed', 0, context, { code: 'network_error' });
66
100
  }
67
- const message = error instanceof Error ? error.message : 'VerifyAI request failed';
101
+ const message = error instanceof Error ? error.message : stringifyErrorMessage(error, 'VerifyAI request failed');
68
102
  return this.buildRequestError(message, 0, context);
69
103
  }
70
104
  async executeRequest(path, options = {}) {
@@ -86,7 +120,7 @@ export class VerifyAIClient {
86
120
  const body = this.parseResponseBody(rawBody);
87
121
  if (!response.ok) {
88
122
  const errorBody = (body && typeof body === 'object' ? body : null);
89
- throw this.buildRequestError(errorBody?.error || `Request failed with status ${response.status}`, response.status, context, {
123
+ throw this.buildRequestError(stringifyErrorMessage(errorBody?.error, `Request failed with status ${response.status}`), response.status, context, {
90
124
  current_usage: errorBody?.current_usage,
91
125
  limit: errorBody?.limit,
92
126
  upgrade_url: errorBody?.upgrade_url,
@@ -220,9 +254,11 @@ export class VerifyAIClient {
220
254
  });
221
255
  }
222
256
  catch (error) {
223
- // Blank 500 means the server crashed during multipart parsing (e.g.
224
- // Vercel function crash). Retry as JSON/base64 with a fresh request
225
- // the crashed request may have partially recorded the idempotency key.
257
+ // 5xx during multipart can mean the server crashed before reservation
258
+ // (e.g. Vercel multipart-parse failure) OR after the verification was
259
+ // saved (e.g. signed-URL generation throw). Retry as base64 JSON and
260
+ // forward the original Idempotency-Key so the server can dedupe instead
261
+ // of inserting a second verify_ai_verifications row.
226
262
  if (error instanceof VerifyAIRequestError && error.status >= 500 && error.isServerError) {
227
263
  try {
228
264
  const { Buffer } = await import('buffer');
@@ -235,7 +271,7 @@ export class VerifyAIClient {
235
271
  metadata: request.metadata,
236
272
  provider: request.provider,
237
273
  include_image_data: request.include_image_data,
238
- });
274
+ }, options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : undefined);
239
275
  }
240
276
  catch {
241
277
  // If the retry itself fails, throw the original error
@@ -316,10 +352,14 @@ export class VerifyAIClient {
316
352
  }
317
353
  export class VerifyAIRequestError extends Error {
318
354
  constructor(message, status, body) {
319
- super(message);
355
+ const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
356
+ super(safeMessage);
320
357
  this.name = 'VerifyAIRequestError';
321
358
  this.status = status;
322
- this.body = body;
359
+ this.body = {
360
+ ...body,
361
+ error: stringifyErrorMessage(body.error, safeMessage),
362
+ };
323
363
  }
324
364
  get isRateLimited() {
325
365
  return this.status === 429;
@@ -4,6 +4,7 @@ import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, AppState,
4
4
  import { CameraView, useCameraPermissions, } from 'expo-camera';
5
5
  import { VerifyAIRequestError } from '../client';
6
6
  import { useTelemetry } from '../telemetry/TelemetryContext';
7
+ import { SDK_VERSION } from '../version';
7
8
  import { BikeOverlay } from './BikeOverlay';
8
9
  import { ScooterOverlay } from './ScooterOverlay';
9
10
  import { ANDROID_ACCELEROMETER_AXIS_DOMINANCE_THRESHOLD, classifyAndroidAccelerometerOrientation, getOverlayRotationDeg, } from './scannerOrientation';
@@ -326,11 +327,23 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
326
327
  telemetryRef.current = telemetry;
327
328
  buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
328
329
  useEffect(() => {
329
- telemetryRef.current?.track('camera_scanner_mounted', {
330
+ const reporter = telemetryRef.current;
331
+ if (reporter) {
332
+ console.log(`VerifyAI[${SDK_VERSION}]: scanner telemetry attached ` +
333
+ `baseUrl=${reporter.getBaseUrl()} apiKeyPrefix=${reporter.getApiKeyPrefix()}…`);
334
+ }
335
+ else {
336
+ console.warn(`VerifyAI[${SDK_VERSION}]: scanner telemetry is not attached; field ` +
337
+ 'diagnostics will only appear in local device logs. Pass a ' +
338
+ '`TelemetryReporter` into `<VerifyAIScanner telemetry={…} />` or ' +
339
+ 'wrap the tree in `<TelemetryContext.Provider value={reporter}>` ' +
340
+ 'to enable server-side diagnostics.');
341
+ }
342
+ reporter?.track('camera_scanner_mounted', {
330
343
  component: 'scanner',
331
344
  error: 'scanner_mounted',
332
345
  metadata: buildScannerTelemetryMetadataRef.current?.({
333
- scanner_telemetry_attached: telemetryRef.current ? 1 : 0,
346
+ scanner_telemetry_attached: reporter ? 1 : 0,
334
347
  }),
335
348
  });
336
349
  return () => {
@@ -9,6 +9,12 @@ export declare class TelemetryReporter {
9
9
  private loadedPersisted;
10
10
  private loadPersistedPromise;
11
11
  constructor(apiKey: string, baseUrl: string);
12
+ /** Telemetry POST destination — exposed so the scanner can log an init banner
13
+ * that helps field debugging confirm where events are being sent. */
14
+ getBaseUrl(): string;
15
+ /** First 6 characters of the API key, for log banners. Never returns the full key. */
16
+ getApiKeyPrefix(): string;
17
+ private logDeliveryFailure;
12
18
  /** Track an error event. Fire-and-forget — never throws. */
13
19
  track(eventType: string, opts?: {
14
20
  component?: string;
@@ -40,6 +40,20 @@ export class TelemetryReporter {
40
40
  // Load any persisted events left behind by a previous session
41
41
  this.loadPersistedBuffer();
42
42
  }
43
+ /** Telemetry POST destination — exposed so the scanner can log an init banner
44
+ * that helps field debugging confirm where events are being sent. */
45
+ getBaseUrl() {
46
+ return this.baseUrl;
47
+ }
48
+ /** First 6 characters of the API key, for log banners. Never returns the full key. */
49
+ getApiKeyPrefix() {
50
+ return this.apiKey.length >= 6 ? this.apiKey.slice(0, 6) : this.apiKey;
51
+ }
52
+ logDeliveryFailure(kind, detail, eventCount) {
53
+ const trimmed = detail.length > 200 ? detail.slice(0, 200) : detail;
54
+ console.warn(`VerifyAI[${SDK_VERSION}]: telemetry POST failed kind=${kind} ` +
55
+ `events=${eventCount} url=${this.baseUrl}/telemetry detail=${trimmed}`);
56
+ }
43
57
  /** Track an error event. Fire-and-forget — never throws. */
44
58
  track(eventType, opts = {}) {
45
59
  if (this.disposed)
@@ -113,6 +127,7 @@ export class TelemetryReporter {
113
127
  this.clearFlushTimer();
114
128
  const controller = new AbortController();
115
129
  const timeout = setTimeout(() => controller.abort(), 10000);
130
+ let loggedDeliveryFailure = false;
116
131
  try {
117
132
  const response = await fetch(`${this.baseUrl}/telemetry`, {
118
133
  method: 'POST',
@@ -124,12 +139,26 @@ export class TelemetryReporter {
124
139
  signal: controller.signal,
125
140
  });
126
141
  if (!response.ok) {
142
+ let body = '';
143
+ try {
144
+ body = await response.text();
145
+ }
146
+ catch {
147
+ // ignore
148
+ }
149
+ this.logDeliveryFailure(`http_${response.status}`, body, events.length);
150
+ loggedDeliveryFailure = true;
127
151
  throw new Error(`Telemetry request failed with status ${response.status}`);
128
152
  }
129
153
  // Success — clear persisted buffer since events are now server-side
130
154
  this.persistBuffer(); // buffer is empty at this point, so this clears the key
131
155
  }
132
- catch {
156
+ catch (error) {
157
+ if (!loggedDeliveryFailure) {
158
+ const kind = error instanceof Error ? error.name : typeof error;
159
+ const detail = error instanceof Error ? error.message : String(error);
160
+ this.logDeliveryFailure(kind, detail, events.length);
161
+ }
133
162
  for (const [dedupKey, event] of bufferedEntries) {
134
163
  const existing = this.buffer.get(dedupKey);
135
164
  if (existing) {
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.4.21";
1
+ export declare const SDK_VERSION = "2.4.23";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.4.21';
1
+ export const SDK_VERSION = '2.4.23';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "2.4.21",
3
+ "version": "2.4.23",
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",
@@ -34,6 +34,44 @@ function enrichMetadata(metadata?: Record<string, unknown>): Record<string, unkn
34
34
  return enriched;
35
35
  }
36
36
 
37
+ function stringifyErrorMessage(value: unknown, fallback: string): string {
38
+ if (typeof value === 'string' && value.trim()) {
39
+ return value;
40
+ }
41
+
42
+ if (value instanceof Error && value.message) {
43
+ return value.message;
44
+ }
45
+
46
+ if (value && typeof value === 'object') {
47
+ const record = value as Record<string, unknown>;
48
+ for (const key of ['message', 'error', 'detail', 'code']) {
49
+ const candidate = record[key];
50
+ if (typeof candidate === 'string' && candidate.trim()) {
51
+ return candidate;
52
+ }
53
+ }
54
+
55
+ try {
56
+ const json = JSON.stringify(value);
57
+ if (json && json !== '{}') {
58
+ return json;
59
+ }
60
+ } catch {
61
+ // Fall through to String(value) below.
62
+ }
63
+ }
64
+
65
+ if (value != null) {
66
+ const text = String(value);
67
+ if (text && text !== '[object Object]') {
68
+ return text;
69
+ }
70
+ }
71
+
72
+ return fallback;
73
+ }
74
+
37
75
  interface RequestContext {
38
76
  path: string;
39
77
  url: string;
@@ -71,13 +109,14 @@ export class VerifyAIClient {
71
109
  }
72
110
 
73
111
  private buildRequestError(
74
- message: string,
112
+ message: unknown,
75
113
  status: number,
76
114
  context: RequestContext,
77
115
  body: Partial<VerifyAIError> = {}
78
116
  ): VerifyAIRequestError {
117
+ const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
79
118
  const error: VerifyAIError = {
80
- error: message,
119
+ error: safeMessage,
81
120
  status,
82
121
  code: status === 408 ? 'timeout' : status === 0 ? 'network_error' : 'request_error',
83
122
  path: context.path,
@@ -86,7 +125,7 @@ export class VerifyAIClient {
86
125
  ...body,
87
126
  };
88
127
 
89
- return new VerifyAIRequestError(message, status, error);
128
+ return new VerifyAIRequestError(safeMessage, status, error);
90
129
  }
91
130
 
92
131
  private normalizeRequestError(error: unknown, context: RequestContext): VerifyAIRequestError {
@@ -102,7 +141,7 @@ export class VerifyAIClient {
102
141
  return this.buildRequestError('Network request failed', 0, context, { code: 'network_error' });
103
142
  }
104
143
 
105
- const message = error instanceof Error ? error.message : 'VerifyAI request failed';
144
+ const message = error instanceof Error ? error.message : stringifyErrorMessage(error, 'VerifyAI request failed');
106
145
  return this.buildRequestError(message, 0, context);
107
146
  }
108
147
 
@@ -132,7 +171,7 @@ export class VerifyAIClient {
132
171
  if (!response.ok) {
133
172
  const errorBody = (body && typeof body === 'object' ? body : null) as VerifyAIError | null;
134
173
  throw this.buildRequestError(
135
- errorBody?.error || `Request failed with status ${response.status}`,
174
+ stringifyErrorMessage(errorBody?.error, `Request failed with status ${response.status}`),
136
175
  response.status,
137
176
  context,
138
177
  {
@@ -284,22 +323,27 @@ export class VerifyAIClient {
284
323
  body: formData,
285
324
  });
286
325
  } catch (error) {
287
- // Blank 500 means the server crashed during multipart parsing (e.g.
288
- // Vercel function crash). Retry as JSON/base64 with a fresh request
289
- // the crashed request may have partially recorded the idempotency key.
326
+ // 5xx during multipart can mean the server crashed before reservation
327
+ // (e.g. Vercel multipart-parse failure) OR after the verification was
328
+ // saved (e.g. signed-URL generation throw). Retry as base64 JSON and
329
+ // forward the original Idempotency-Key so the server can dedupe instead
330
+ // of inserting a second verify_ai_verifications row.
290
331
  if (error instanceof VerifyAIRequestError && error.status >= 500 && error.isServerError) {
291
332
  try {
292
333
  const { Buffer } = await import('buffer');
293
334
  const fileResponse = await fetch(request.imageUri);
294
335
  const arrayBuffer = await fileResponse.arrayBuffer();
295
336
  const base64 = Buffer.from(arrayBuffer).toString('base64');
296
- return this.verify({
297
- image: base64,
298
- policy: request.policy,
299
- metadata: request.metadata,
300
- provider: request.provider,
301
- include_image_data: request.include_image_data,
302
- });
337
+ return this.verify(
338
+ {
339
+ image: base64,
340
+ policy: request.policy,
341
+ metadata: request.metadata,
342
+ provider: request.provider,
343
+ include_image_data: request.include_image_data,
344
+ },
345
+ options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : undefined,
346
+ );
303
347
  } catch {
304
348
  // If the retry itself fails, throw the original error
305
349
  throw error;
@@ -384,11 +428,15 @@ export class VerifyAIRequestError extends Error {
384
428
  status: number;
385
429
  body: VerifyAIError;
386
430
 
387
- constructor(message: string, status: number, body: VerifyAIError) {
388
- super(message);
431
+ constructor(message: unknown, status: number, body: VerifyAIError) {
432
+ const safeMessage = stringifyErrorMessage(message, 'VerifyAI request failed');
433
+ super(safeMessage);
389
434
  this.name = 'VerifyAIRequestError';
390
435
  this.status = status;
391
- this.body = body;
436
+ this.body = {
437
+ ...body,
438
+ error: stringifyErrorMessage(body.error, safeMessage),
439
+ };
392
440
  }
393
441
 
394
442
  get isRateLimited(): boolean {
@@ -23,6 +23,7 @@ import type {
23
23
  import { VerifyAIRequestError } from '../client';
24
24
  import { useTelemetry } from '../telemetry/TelemetryContext';
25
25
  import type { TelemetryReporter } from '../telemetry/TelemetryReporter';
26
+ import { SDK_VERSION } from '../version';
26
27
  import { BikeOverlay } from './BikeOverlay';
27
28
  import { ScooterOverlay } from './ScooterOverlay';
28
29
  import {
@@ -439,11 +440,26 @@ export function VerifyAIScanner({
439
440
  buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
440
441
 
441
442
  useEffect(() => {
442
- telemetryRef.current?.track('camera_scanner_mounted', {
443
+ const reporter = telemetryRef.current;
444
+ if (reporter) {
445
+ console.log(
446
+ `VerifyAI[${SDK_VERSION}]: scanner telemetry attached ` +
447
+ `baseUrl=${reporter.getBaseUrl()} apiKeyPrefix=${reporter.getApiKeyPrefix()}…`,
448
+ );
449
+ } else {
450
+ console.warn(
451
+ `VerifyAI[${SDK_VERSION}]: scanner telemetry is not attached; field ` +
452
+ 'diagnostics will only appear in local device logs. Pass a ' +
453
+ '`TelemetryReporter` into `<VerifyAIScanner telemetry={…} />` or ' +
454
+ 'wrap the tree in `<TelemetryContext.Provider value={reporter}>` ' +
455
+ 'to enable server-side diagnostics.',
456
+ );
457
+ }
458
+ reporter?.track('camera_scanner_mounted', {
443
459
  component: 'scanner',
444
460
  error: 'scanner_mounted',
445
461
  metadata: buildScannerTelemetryMetadataRef.current?.({
446
- scanner_telemetry_attached: telemetryRef.current ? 1 : 0,
462
+ scanner_telemetry_attached: reporter ? 1 : 0,
447
463
  }),
448
464
  });
449
465
 
@@ -71,6 +71,25 @@ export class TelemetryReporter {
71
71
  this.loadPersistedBuffer();
72
72
  }
73
73
 
74
+ /** Telemetry POST destination — exposed so the scanner can log an init banner
75
+ * that helps field debugging confirm where events are being sent. */
76
+ getBaseUrl(): string {
77
+ return this.baseUrl;
78
+ }
79
+
80
+ /** First 6 characters of the API key, for log banners. Never returns the full key. */
81
+ getApiKeyPrefix(): string {
82
+ return this.apiKey.length >= 6 ? this.apiKey.slice(0, 6) : this.apiKey;
83
+ }
84
+
85
+ private logDeliveryFailure(kind: string, detail: string, eventCount: number): void {
86
+ const trimmed = detail.length > 200 ? detail.slice(0, 200) : detail;
87
+ console.warn(
88
+ `VerifyAI[${SDK_VERSION}]: telemetry POST failed kind=${kind} ` +
89
+ `events=${eventCount} url=${this.baseUrl}/telemetry detail=${trimmed}`,
90
+ );
91
+ }
92
+
74
93
  /** Track an error event. Fire-and-forget — never throws. */
75
94
  track(
76
95
  eventType: string,
@@ -158,6 +177,7 @@ export class TelemetryReporter {
158
177
  const controller = new AbortController();
159
178
  const timeout = setTimeout(() => controller.abort(), 10000);
160
179
 
180
+ let loggedDeliveryFailure = false;
161
181
  try {
162
182
  const response = await fetch(`${this.baseUrl}/telemetry`, {
163
183
  method: 'POST',
@@ -170,12 +190,25 @@ export class TelemetryReporter {
170
190
  });
171
191
 
172
192
  if (!response.ok) {
193
+ let body = '';
194
+ try {
195
+ body = await response.text();
196
+ } catch {
197
+ // ignore
198
+ }
199
+ this.logDeliveryFailure(`http_${response.status}`, body, events.length);
200
+ loggedDeliveryFailure = true;
173
201
  throw new Error(`Telemetry request failed with status ${response.status}`);
174
202
  }
175
203
 
176
204
  // Success — clear persisted buffer since events are now server-side
177
205
  this.persistBuffer(); // buffer is empty at this point, so this clears the key
178
- } catch {
206
+ } catch (error) {
207
+ if (!loggedDeliveryFailure) {
208
+ const kind = error instanceof Error ? error.name : typeof error;
209
+ const detail = error instanceof Error ? error.message : String(error);
210
+ this.logDeliveryFailure(kind, detail, events.length);
211
+ }
179
212
  for (const [dedupKey, event] of bufferedEntries) {
180
213
  const existing = this.buffer.get(dedupKey);
181
214
  if (existing) {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.4.21';
1
+ export const SDK_VERSION = '2.4.23';