@switchlabs/verify-ai-react-native 2.5.1 → 2.5.2
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/lib/client/index.js +54 -7
- package/lib/components/VerifyAIScanner.js +61 -5
- package/lib/types/index.d.ts +2 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +73 -7
- package/src/components/VerifyAIScanner.tsx +65 -5
- package/src/types/index.ts +2 -0
- package/src/version.ts +1 -1
package/lib/client/index.js
CHANGED
|
@@ -52,6 +52,49 @@ function stringifyErrorMessage(value, fallback) {
|
|
|
52
52
|
}
|
|
53
53
|
return fallback;
|
|
54
54
|
}
|
|
55
|
+
const VERIFY_METADATA_TELEMETRY_KEYS = [
|
|
56
|
+
'rideId',
|
|
57
|
+
'vehicleType',
|
|
58
|
+
'verificationAttemptId',
|
|
59
|
+
'verificationAttempt',
|
|
60
|
+
'verificationMaxAttempts',
|
|
61
|
+
'verificationSource',
|
|
62
|
+
'torchEnabled',
|
|
63
|
+
'scannerSessionId',
|
|
64
|
+
];
|
|
65
|
+
function toTelemetryKey(key) {
|
|
66
|
+
return key
|
|
67
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
68
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
69
|
+
.replace(/^_+|_+$/g, '')
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
function compactClientTelemetryMetadata(metadata = {}) {
|
|
73
|
+
const compacted = {};
|
|
74
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
75
|
+
if (value === null || value === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
compacted[key] = typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
78
|
+
}
|
|
79
|
+
return compacted;
|
|
80
|
+
}
|
|
81
|
+
function buildVerifyTelemetryMetadata(requestMetadata, options) {
|
|
82
|
+
const metadata = {
|
|
83
|
+
...options?.telemetryMetadata,
|
|
84
|
+
};
|
|
85
|
+
if (options?.idempotencyKey) {
|
|
86
|
+
metadata.idempotency_key = options.idempotencyKey;
|
|
87
|
+
}
|
|
88
|
+
for (const key of VERIFY_METADATA_TELEMETRY_KEYS) {
|
|
89
|
+
const value = requestMetadata?.[key];
|
|
90
|
+
if (typeof value === 'string' ||
|
|
91
|
+
typeof value === 'number' ||
|
|
92
|
+
typeof value === 'boolean') {
|
|
93
|
+
metadata[`verify_metadata_${toTelemetryKey(key)}`] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return metadata;
|
|
97
|
+
}
|
|
55
98
|
export class VerifyAIClient {
|
|
56
99
|
constructor(config) {
|
|
57
100
|
if (!config.apiKey) {
|
|
@@ -101,7 +144,7 @@ export class VerifyAIClient {
|
|
|
101
144
|
const message = error instanceof Error ? error.message : stringifyErrorMessage(error, 'VerifyAI request failed');
|
|
102
145
|
return this.buildRequestError(message, 0, context);
|
|
103
146
|
}
|
|
104
|
-
async executeRequest(path, options = {}) {
|
|
147
|
+
async executeRequest(path, options = {}, telemetryMetadata = {}) {
|
|
105
148
|
const controller = new AbortController();
|
|
106
149
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
107
150
|
const method = (options.method || 'GET').toUpperCase();
|
|
@@ -156,6 +199,7 @@ export class VerifyAIClient {
|
|
|
156
199
|
? `${normalized.message} [${context.method} ${context.path}; timeout=${this.timeout}ms]`
|
|
157
200
|
: normalized;
|
|
158
201
|
const metadata = {
|
|
202
|
+
...compactClientTelemetryMetadata(telemetryMetadata),
|
|
159
203
|
method: context.method,
|
|
160
204
|
path: context.path,
|
|
161
205
|
status: normalized.status,
|
|
@@ -178,8 +222,8 @@ export class VerifyAIClient {
|
|
|
178
222
|
clearTimeout(timer);
|
|
179
223
|
}
|
|
180
224
|
}
|
|
181
|
-
async request(path, options = {}) {
|
|
182
|
-
return this.executeRequest(path, options);
|
|
225
|
+
async request(path, options = {}, telemetryMetadata = {}) {
|
|
226
|
+
return this.executeRequest(path, options, telemetryMetadata);
|
|
183
227
|
}
|
|
184
228
|
/**
|
|
185
229
|
* Submit a photo for AI verification.
|
|
@@ -209,11 +253,12 @@ export class VerifyAIClient {
|
|
|
209
253
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
210
254
|
}
|
|
211
255
|
const enrichedRequest = { ...request, metadata: enrichMetadata(request.metadata) };
|
|
256
|
+
const telemetryMetadata = buildVerifyTelemetryMetadata(enrichedRequest.metadata, options);
|
|
212
257
|
return this.request('/verify', {
|
|
213
258
|
method: 'POST',
|
|
214
259
|
headers,
|
|
215
260
|
body: JSON.stringify(enrichedRequest),
|
|
216
|
-
});
|
|
261
|
+
}, telemetryMetadata);
|
|
217
262
|
}
|
|
218
263
|
/**
|
|
219
264
|
* Submit a photo for AI verification using multipart/form-data.
|
|
@@ -232,7 +277,8 @@ export class VerifyAIClient {
|
|
|
232
277
|
name: 'photo.jpg',
|
|
233
278
|
});
|
|
234
279
|
formData.append('policy', request.policy);
|
|
235
|
-
|
|
280
|
+
const enrichedMetadata = enrichMetadata(request.metadata);
|
|
281
|
+
formData.append('metadata', JSON.stringify(enrichedMetadata));
|
|
236
282
|
if (request.provider) {
|
|
237
283
|
formData.append('provider', request.provider);
|
|
238
284
|
}
|
|
@@ -246,12 +292,13 @@ export class VerifyAIClient {
|
|
|
246
292
|
if (options?.idempotencyKey) {
|
|
247
293
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
248
294
|
}
|
|
295
|
+
const telemetryMetadata = buildVerifyTelemetryMetadata(enrichedMetadata, options);
|
|
249
296
|
try {
|
|
250
297
|
return await this.executeRequest('/verify', {
|
|
251
298
|
method: 'POST',
|
|
252
299
|
headers,
|
|
253
300
|
body: formData,
|
|
254
|
-
});
|
|
301
|
+
}, telemetryMetadata);
|
|
255
302
|
}
|
|
256
303
|
catch (error) {
|
|
257
304
|
// 5xx during multipart can mean the server crashed before reservation
|
|
@@ -271,7 +318,7 @@ export class VerifyAIClient {
|
|
|
271
318
|
metadata: request.metadata,
|
|
272
319
|
provider: request.provider,
|
|
273
320
|
include_image_data: request.include_image_data,
|
|
274
|
-
}, options
|
|
321
|
+
}, options);
|
|
275
322
|
}
|
|
276
323
|
catch {
|
|
277
324
|
// If the retry itself fails, throw the original error
|
|
@@ -20,6 +20,7 @@ const CAMERA_INIT_ERROR_CODE = 'ERR_CAMERA_INIT_FAILED';
|
|
|
20
20
|
const DEFAULT_TERMINAL_RESULT_DISPLAY_MS = 3000;
|
|
21
21
|
const TRANSIENT_ERROR_DISPLAY_MS = 3000;
|
|
22
22
|
const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
|
|
23
|
+
const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
|
|
23
24
|
const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
|
|
24
25
|
const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
|
|
25
26
|
const IOS_CAMERA_STARTUP_WATCHDOG_MS = 8000;
|
|
@@ -76,6 +77,9 @@ function createScannerError(message, code, name) {
|
|
|
76
77
|
function sleep(ms) {
|
|
77
78
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
78
79
|
}
|
|
80
|
+
function createScannerSessionId() {
|
|
81
|
+
return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
82
|
+
}
|
|
79
83
|
function isNativeCameraCaptureError(error) {
|
|
80
84
|
const message = error.message.toLowerCase();
|
|
81
85
|
return (error.code === CAMERA_CAPTURE_ERROR_CODE ||
|
|
@@ -261,7 +265,12 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
261
265
|
const [physicalOrientation, setPhysicalOrientation] = useState('portrait');
|
|
262
266
|
physicalOrientationRef.current = physicalOrientation;
|
|
263
267
|
const overlayRotationDeg = getOverlayRotationDeg(physicalOrientation);
|
|
268
|
+
const scannerSessionIdRef = useRef(createScannerSessionId());
|
|
269
|
+
const captureSequenceRef = useRef(0);
|
|
270
|
+
const torchRetrySuppressedRef = useRef(false);
|
|
271
|
+
const [torchRetrySuppressed, setTorchRetrySuppressed] = useState(false);
|
|
264
272
|
const buildScannerTelemetryMetadata = useCallback((extra = {}) => compactTelemetryMetadata({
|
|
273
|
+
scanner_session_id: scannerSessionIdRef.current,
|
|
265
274
|
policy,
|
|
266
275
|
status,
|
|
267
276
|
camera_ready: cameraReadyRef.current,
|
|
@@ -290,6 +299,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
290
299
|
last_orientation_remount_at: lastOrientationRemountAtRef.current,
|
|
291
300
|
last_capture_retry_at: lastCaptureRetryAtRef.current,
|
|
292
301
|
requested_torch_enabled: enableTorch ? 1 : 0,
|
|
302
|
+
torch_retry_suppressed: torchRetrySuppressedRef.current,
|
|
293
303
|
android_native_orientation_subscription_active: 0,
|
|
294
304
|
android_native_orientation_event_count: 0,
|
|
295
305
|
android_native_orientation_change_count: 0,
|
|
@@ -342,6 +352,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
342
352
|
]);
|
|
343
353
|
telemetryRef.current = telemetry;
|
|
344
354
|
buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
|
|
355
|
+
const setTorchSuppressedForRetry = useCallback((nextSuppressed) => {
|
|
356
|
+
torchRetrySuppressedRef.current = nextSuppressed;
|
|
357
|
+
setTorchRetrySuppressed(nextSuppressed);
|
|
358
|
+
}, []);
|
|
345
359
|
useEffect(() => {
|
|
346
360
|
const reporter = telemetryRef.current;
|
|
347
361
|
if (reporter) {
|
|
@@ -382,7 +396,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
382
396
|
cameraStartupAttemptStartedAtRef.current = Date.now();
|
|
383
397
|
if (opts.log) {
|
|
384
398
|
console.warn(`VerifyAI[${SDK_VERSION}]: remounting camera reason=${reason} ` +
|
|
385
|
-
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0}`
|
|
399
|
+
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0} ` +
|
|
400
|
+
`torchRetrySuppressed=${torchRetrySuppressedRef.current ? 1 : 0}`);
|
|
386
401
|
}
|
|
387
402
|
const remount = () => {
|
|
388
403
|
setCameraKey((k) => k + 1);
|
|
@@ -875,6 +890,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
875
890
|
}
|
|
876
891
|
}, [buildScannerTelemetryMetadata, permission, telemetry]);
|
|
877
892
|
const handleCapture = useCallback(async () => {
|
|
893
|
+
const captureSequence = captureSequenceRef.current + 1;
|
|
894
|
+
captureSequenceRef.current = captureSequence;
|
|
895
|
+
const captureAttemptId = `${scannerSessionIdRef.current}_cap_${captureSequence}`;
|
|
878
896
|
const blockedReason = !cameraRef.current
|
|
879
897
|
? 'camera_ref_null'
|
|
880
898
|
: !cameraReadyRef.current
|
|
@@ -892,6 +910,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
892
910
|
component: 'scanner',
|
|
893
911
|
error: blockedReason == null ? 'capture_requested' : 'capture_ignored',
|
|
894
912
|
metadata: buildScannerTelemetryMetadata({
|
|
913
|
+
capture_attempt_id: captureAttemptId,
|
|
914
|
+
capture_sequence: captureSequence,
|
|
895
915
|
capture_blocked_reason: blockedReason,
|
|
896
916
|
}),
|
|
897
917
|
});
|
|
@@ -912,6 +932,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
912
932
|
return;
|
|
913
933
|
}
|
|
914
934
|
setStatus('capturing');
|
|
935
|
+
setTorchSuppressedForRetry(false);
|
|
915
936
|
setResult(null);
|
|
916
937
|
setLastError(null);
|
|
917
938
|
terminalResultRef.current = null;
|
|
@@ -923,6 +944,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
923
944
|
let nativeCaptureAttempts = 0;
|
|
924
945
|
let captureRetryAttempted = false;
|
|
925
946
|
let captureRetryReady = false;
|
|
947
|
+
let captureRetryTorchSuppressed = false;
|
|
948
|
+
let captureRetrySettleDelayMs = CAMERA_CAPTURE_RETRY_DELAY_MS;
|
|
949
|
+
let captureRetryRemountBackoffMs = 0;
|
|
926
950
|
let lastNativeCaptureErrorMessage = null;
|
|
927
951
|
try {
|
|
928
952
|
const capturePhysicalOrientation = physicalOrientation;
|
|
@@ -931,6 +955,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
931
955
|
component: 'scanner',
|
|
932
956
|
error: 'capture_orientation_context',
|
|
933
957
|
metadata: buildScannerTelemetryMetadata({
|
|
958
|
+
capture_attempt_id: captureAttemptId,
|
|
959
|
+
capture_sequence: captureSequence,
|
|
934
960
|
capture_physical_orientation: capturePhysicalOrientation,
|
|
935
961
|
capture_overlay_rotation_deg: captureOverlayRotationDeg,
|
|
936
962
|
capture_rotation_applied: 0,
|
|
@@ -988,11 +1014,23 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
988
1014
|
lastNativeCaptureErrorMessage = normalized.message;
|
|
989
1015
|
if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
|
|
990
1016
|
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;
|
|
991
1024
|
const retryAt = new Date().toISOString();
|
|
992
1025
|
lastCaptureRetryAtRef.current = retryAt;
|
|
993
1026
|
cameraRemountCountRef.current++;
|
|
994
|
-
|
|
995
|
-
|
|
1027
|
+
if (captureRetryTorchSuppressed) {
|
|
1028
|
+
setTorchSuppressedForRetry(true);
|
|
1029
|
+
}
|
|
1030
|
+
requestCameraRemount('capture_retry', captureRetryRemountBackoffMs > 0
|
|
1031
|
+
? { backoffMs: captureRetryRemountBackoffMs }
|
|
1032
|
+
: undefined);
|
|
1033
|
+
await sleep(captureRetrySettleDelayMs);
|
|
996
1034
|
const captureRetryReadyTimeoutMs = Platform.OS === 'android'
|
|
997
1035
|
? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
|
|
998
1036
|
: IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
|
|
@@ -1002,11 +1040,16 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1002
1040
|
error: normalized,
|
|
1003
1041
|
errorCode: normalized.code,
|
|
1004
1042
|
metadata: buildScannerTelemetryMetadata({
|
|
1043
|
+
capture_attempt_id: captureAttemptId,
|
|
1044
|
+
capture_sequence: captureSequence,
|
|
1005
1045
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1006
1046
|
capture_retry_attempted: captureRetryAttempted,
|
|
1007
1047
|
capture_retry_ready: captureRetryReady,
|
|
1008
1048
|
capture_retry_delay_ms: CAMERA_CAPTURE_RETRY_DELAY_MS,
|
|
1049
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1050
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1009
1051
|
capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
|
|
1052
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1010
1053
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1011
1054
|
}),
|
|
1012
1055
|
});
|
|
@@ -1074,6 +1117,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1074
1117
|
telemetry?.track('image_processed', {
|
|
1075
1118
|
component: 'scanner',
|
|
1076
1119
|
metadata: buildScannerTelemetryMetadata({
|
|
1120
|
+
capture_attempt_id: captureAttemptId,
|
|
1121
|
+
capture_sequence: captureSequence,
|
|
1077
1122
|
original_width: origWidth,
|
|
1078
1123
|
original_height: origHeight,
|
|
1079
1124
|
processed_width: processedWidth,
|
|
@@ -1083,6 +1128,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1083
1128
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1084
1129
|
capture_retry_attempted: captureRetryAttempted,
|
|
1085
1130
|
capture_retry_ready: captureRetryReady,
|
|
1131
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1132
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1133
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1086
1134
|
recovered_native_capture_failure: captureRetryAttempted ? 1 : 0,
|
|
1087
1135
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1088
1136
|
}),
|
|
@@ -1151,9 +1199,14 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1151
1199
|
component: 'scanner',
|
|
1152
1200
|
error,
|
|
1153
1201
|
metadata: buildScannerTelemetryMetadata({
|
|
1202
|
+
capture_attempt_id: captureAttemptId,
|
|
1203
|
+
capture_sequence: captureSequence,
|
|
1154
1204
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1155
1205
|
capture_retry_attempted: captureRetryAttempted,
|
|
1156
1206
|
capture_retry_ready: captureRetryReady,
|
|
1207
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1208
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1209
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1157
1210
|
is_native_camera_capture_error: isNativeCameraCaptureError(error),
|
|
1158
1211
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1159
1212
|
}),
|
|
@@ -1163,7 +1216,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1163
1216
|
setTimeout(() => setStatus('idle'), TRANSIENT_ERROR_DISPLAY_MS);
|
|
1164
1217
|
}
|
|
1165
1218
|
}
|
|
1166
|
-
|
|
1219
|
+
finally {
|
|
1220
|
+
setTorchSuppressedForRetry(false);
|
|
1221
|
+
}
|
|
1222
|
+
}, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg, enableTorch, setTorchSuppressedForRetry]);
|
|
1167
1223
|
// Expose capture to parent via ref
|
|
1168
1224
|
if (captureRef) {
|
|
1169
1225
|
captureRef.current = handleCapture;
|
|
@@ -1175,7 +1231,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1175
1231
|
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" }) })] }));
|
|
1176
1232
|
}
|
|
1177
1233
|
const showBottomCard = status === 'success' || status === 'error';
|
|
1178
|
-
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch;
|
|
1234
|
+
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch && !torchRetrySuppressed;
|
|
1179
1235
|
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) => {
|
|
1180
1236
|
setPhysicalOrientation(event.orientation);
|
|
1181
1237
|
}, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: [styles.topBar, isLandscape && styles.topBarLandscape], children: _jsx(Text, { style: [
|
package/lib/types/index.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface VerificationListParams {
|
|
|
57
57
|
export interface VerifyOptions {
|
|
58
58
|
/** Idempotency key to prevent duplicate verifications on retry. */
|
|
59
59
|
idempotencyKey?: string;
|
|
60
|
+
/** Optional primitive metadata attached to client-side request telemetry. */
|
|
61
|
+
telemetryMetadata?: Record<string, string | number | boolean | null | undefined>;
|
|
60
62
|
}
|
|
61
63
|
export interface QueueItem {
|
|
62
64
|
id: string;
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.5.
|
|
1
|
+
export declare const SDK_VERSION = "2.5.2";
|
package/lib/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.5.
|
|
1
|
+
export const SDK_VERSION = '2.5.2';
|
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -78,6 +78,65 @@ interface RequestContext {
|
|
|
78
78
|
method: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
type ClientTelemetryValue = string | number | boolean | null | undefined;
|
|
82
|
+
type ClientTelemetryMetadata = Record<string, ClientTelemetryValue>;
|
|
83
|
+
|
|
84
|
+
const VERIFY_METADATA_TELEMETRY_KEYS = [
|
|
85
|
+
'rideId',
|
|
86
|
+
'vehicleType',
|
|
87
|
+
'verificationAttemptId',
|
|
88
|
+
'verificationAttempt',
|
|
89
|
+
'verificationMaxAttempts',
|
|
90
|
+
'verificationSource',
|
|
91
|
+
'torchEnabled',
|
|
92
|
+
'scannerSessionId',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
function toTelemetryKey(key: string): string {
|
|
96
|
+
return key
|
|
97
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
98
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
99
|
+
.replace(/^_+|_+$/g, '')
|
|
100
|
+
.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compactClientTelemetryMetadata(
|
|
104
|
+
metadata: ClientTelemetryMetadata = {},
|
|
105
|
+
): Record<string, string | number> {
|
|
106
|
+
const compacted: Record<string, string | number> = {};
|
|
107
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
108
|
+
if (value === null || value === undefined) continue;
|
|
109
|
+
compacted[key] = typeof value === 'boolean' ? (value ? 1 : 0) : value;
|
|
110
|
+
}
|
|
111
|
+
return compacted;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildVerifyTelemetryMetadata(
|
|
115
|
+
requestMetadata: Record<string, unknown> | undefined,
|
|
116
|
+
options?: VerifyOptions,
|
|
117
|
+
): ClientTelemetryMetadata {
|
|
118
|
+
const metadata: ClientTelemetryMetadata = {
|
|
119
|
+
...options?.telemetryMetadata,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (options?.idempotencyKey) {
|
|
123
|
+
metadata.idempotency_key = options.idempotencyKey;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const key of VERIFY_METADATA_TELEMETRY_KEYS) {
|
|
127
|
+
const value = requestMetadata?.[key];
|
|
128
|
+
if (
|
|
129
|
+
typeof value === 'string' ||
|
|
130
|
+
typeof value === 'number' ||
|
|
131
|
+
typeof value === 'boolean'
|
|
132
|
+
) {
|
|
133
|
+
metadata[`verify_metadata_${toTelemetryKey(key)}`] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return metadata;
|
|
138
|
+
}
|
|
139
|
+
|
|
81
140
|
export class VerifyAIClient {
|
|
82
141
|
private apiKey: string;
|
|
83
142
|
private baseUrl: string;
|
|
@@ -147,7 +206,8 @@ export class VerifyAIClient {
|
|
|
147
206
|
|
|
148
207
|
private async executeRequest<T>(
|
|
149
208
|
path: string,
|
|
150
|
-
options: RequestInit = {}
|
|
209
|
+
options: RequestInit = {},
|
|
210
|
+
telemetryMetadata: ClientTelemetryMetadata = {},
|
|
151
211
|
): Promise<T> {
|
|
152
212
|
const controller = new AbortController();
|
|
153
213
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -218,6 +278,7 @@ export class VerifyAIClient {
|
|
|
218
278
|
? `${normalized.message} [${context.method} ${context.path}; timeout=${this.timeout}ms]`
|
|
219
279
|
: normalized;
|
|
220
280
|
const metadata: Record<string, string | number> = {
|
|
281
|
+
...compactClientTelemetryMetadata(telemetryMetadata),
|
|
221
282
|
method: context.method,
|
|
222
283
|
path: context.path,
|
|
223
284
|
status: normalized.status,
|
|
@@ -243,9 +304,10 @@ export class VerifyAIClient {
|
|
|
243
304
|
|
|
244
305
|
private async request<T>(
|
|
245
306
|
path: string,
|
|
246
|
-
options: RequestInit = {}
|
|
307
|
+
options: RequestInit = {},
|
|
308
|
+
telemetryMetadata: ClientTelemetryMetadata = {},
|
|
247
309
|
): Promise<T> {
|
|
248
|
-
return this.executeRequest<T>(path, options);
|
|
310
|
+
return this.executeRequest<T>(path, options, telemetryMetadata);
|
|
249
311
|
}
|
|
250
312
|
|
|
251
313
|
/**
|
|
@@ -276,11 +338,12 @@ export class VerifyAIClient {
|
|
|
276
338
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
277
339
|
}
|
|
278
340
|
const enrichedRequest = { ...request, metadata: enrichMetadata(request.metadata) };
|
|
341
|
+
const telemetryMetadata = buildVerifyTelemetryMetadata(enrichedRequest.metadata, options);
|
|
279
342
|
return this.request<VerificationResult>('/verify', {
|
|
280
343
|
method: 'POST',
|
|
281
344
|
headers,
|
|
282
345
|
body: JSON.stringify(enrichedRequest),
|
|
283
|
-
});
|
|
346
|
+
}, telemetryMetadata);
|
|
284
347
|
}
|
|
285
348
|
|
|
286
349
|
/**
|
|
@@ -300,7 +363,8 @@ export class VerifyAIClient {
|
|
|
300
363
|
name: 'photo.jpg',
|
|
301
364
|
} as unknown as Blob);
|
|
302
365
|
formData.append('policy', request.policy);
|
|
303
|
-
|
|
366
|
+
const enrichedMetadata = enrichMetadata(request.metadata);
|
|
367
|
+
formData.append('metadata', JSON.stringify(enrichedMetadata));
|
|
304
368
|
if (request.provider) {
|
|
305
369
|
formData.append('provider', request.provider);
|
|
306
370
|
}
|
|
@@ -316,12 +380,14 @@ export class VerifyAIClient {
|
|
|
316
380
|
headers['Idempotency-Key'] = options.idempotencyKey;
|
|
317
381
|
}
|
|
318
382
|
|
|
383
|
+
const telemetryMetadata = buildVerifyTelemetryMetadata(enrichedMetadata, options);
|
|
384
|
+
|
|
319
385
|
try {
|
|
320
386
|
return await this.executeRequest<VerificationResult>('/verify', {
|
|
321
387
|
method: 'POST',
|
|
322
388
|
headers,
|
|
323
389
|
body: formData,
|
|
324
|
-
});
|
|
390
|
+
}, telemetryMetadata);
|
|
325
391
|
} catch (error) {
|
|
326
392
|
// 5xx during multipart can mean the server crashed before reservation
|
|
327
393
|
// (e.g. Vercel multipart-parse failure) OR after the verification was
|
|
@@ -342,7 +408,7 @@ export class VerifyAIClient {
|
|
|
342
408
|
provider: request.provider,
|
|
343
409
|
include_image_data: request.include_image_data,
|
|
344
410
|
},
|
|
345
|
-
options
|
|
411
|
+
options,
|
|
346
412
|
);
|
|
347
413
|
} catch {
|
|
348
414
|
// If the retry itself fails, throw the original error
|
|
@@ -45,6 +45,7 @@ const CAMERA_INIT_ERROR_CODE = 'ERR_CAMERA_INIT_FAILED';
|
|
|
45
45
|
const DEFAULT_TERMINAL_RESULT_DISPLAY_MS = 3000;
|
|
46
46
|
const TRANSIENT_ERROR_DISPLAY_MS = 3000;
|
|
47
47
|
const CAMERA_CAPTURE_RETRY_DELAY_MS = 350;
|
|
48
|
+
const CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS = 800;
|
|
48
49
|
const IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 3000;
|
|
49
50
|
const ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS = 10000;
|
|
50
51
|
const IOS_CAMERA_STARTUP_WATCHDOG_MS = 8000;
|
|
@@ -153,6 +154,10 @@ function sleep(ms: number): Promise<void> {
|
|
|
153
154
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
function createScannerSessionId(): string {
|
|
158
|
+
return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
156
161
|
function isNativeCameraCaptureError(error: ErrorWithDetails): boolean {
|
|
157
162
|
const message = error.message.toLowerCase();
|
|
158
163
|
return (
|
|
@@ -369,10 +374,15 @@ export function VerifyAIScanner({
|
|
|
369
374
|
|
|
370
375
|
physicalOrientationRef.current = physicalOrientation;
|
|
371
376
|
const overlayRotationDeg = getOverlayRotationDeg(physicalOrientation);
|
|
377
|
+
const scannerSessionIdRef = useRef(createScannerSessionId());
|
|
378
|
+
const captureSequenceRef = useRef(0);
|
|
379
|
+
const torchRetrySuppressedRef = useRef(false);
|
|
380
|
+
const [torchRetrySuppressed, setTorchRetrySuppressed] = useState(false);
|
|
372
381
|
|
|
373
382
|
const buildScannerTelemetryMetadata = useCallback((
|
|
374
383
|
extra: Record<string, string | number | boolean | null | undefined> = {},
|
|
375
384
|
): ScannerTelemetryMetadata => compactTelemetryMetadata({
|
|
385
|
+
scanner_session_id: scannerSessionIdRef.current,
|
|
376
386
|
policy,
|
|
377
387
|
status,
|
|
378
388
|
camera_ready: cameraReadyRef.current,
|
|
@@ -401,6 +411,7 @@ export function VerifyAIScanner({
|
|
|
401
411
|
last_orientation_remount_at: lastOrientationRemountAtRef.current,
|
|
402
412
|
last_capture_retry_at: lastCaptureRetryAtRef.current,
|
|
403
413
|
requested_torch_enabled: enableTorch ? 1 : 0,
|
|
414
|
+
torch_retry_suppressed: torchRetrySuppressedRef.current,
|
|
404
415
|
android_native_orientation_subscription_active: 0,
|
|
405
416
|
android_native_orientation_event_count: 0,
|
|
406
417
|
android_native_orientation_change_count: 0,
|
|
@@ -455,6 +466,11 @@ export function VerifyAIScanner({
|
|
|
455
466
|
telemetryRef.current = telemetry;
|
|
456
467
|
buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
|
|
457
468
|
|
|
469
|
+
const setTorchSuppressedForRetry = useCallback((nextSuppressed: boolean) => {
|
|
470
|
+
torchRetrySuppressedRef.current = nextSuppressed;
|
|
471
|
+
setTorchRetrySuppressed(nextSuppressed);
|
|
472
|
+
}, []);
|
|
473
|
+
|
|
458
474
|
useEffect(() => {
|
|
459
475
|
const reporter = telemetryRef.current;
|
|
460
476
|
if (reporter) {
|
|
@@ -509,7 +525,8 @@ export function VerifyAIScanner({
|
|
|
509
525
|
if (opts.log) {
|
|
510
526
|
console.warn(
|
|
511
527
|
`VerifyAI[${SDK_VERSION}]: remounting camera reason=${reason} ` +
|
|
512
|
-
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0}
|
|
528
|
+
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0} ` +
|
|
529
|
+
`torchRetrySuppressed=${torchRetrySuppressedRef.current ? 1 : 0}`,
|
|
513
530
|
);
|
|
514
531
|
}
|
|
515
532
|
|
|
@@ -1076,6 +1093,9 @@ export function VerifyAIScanner({
|
|
|
1076
1093
|
}, [buildScannerTelemetryMetadata, permission, telemetry]);
|
|
1077
1094
|
|
|
1078
1095
|
const handleCapture = useCallback(async () => {
|
|
1096
|
+
const captureSequence = captureSequenceRef.current + 1;
|
|
1097
|
+
captureSequenceRef.current = captureSequence;
|
|
1098
|
+
const captureAttemptId = `${scannerSessionIdRef.current}_cap_${captureSequence}`;
|
|
1079
1099
|
const blockedReason =
|
|
1080
1100
|
!cameraRef.current
|
|
1081
1101
|
? 'camera_ref_null'
|
|
@@ -1095,6 +1115,8 @@ export function VerifyAIScanner({
|
|
|
1095
1115
|
component: 'scanner',
|
|
1096
1116
|
error: blockedReason == null ? 'capture_requested' : 'capture_ignored',
|
|
1097
1117
|
metadata: buildScannerTelemetryMetadata({
|
|
1118
|
+
capture_attempt_id: captureAttemptId,
|
|
1119
|
+
capture_sequence: captureSequence,
|
|
1098
1120
|
capture_blocked_reason: blockedReason,
|
|
1099
1121
|
}),
|
|
1100
1122
|
});
|
|
@@ -1120,6 +1142,7 @@ export function VerifyAIScanner({
|
|
|
1120
1142
|
}
|
|
1121
1143
|
|
|
1122
1144
|
setStatus('capturing');
|
|
1145
|
+
setTorchSuppressedForRetry(false);
|
|
1123
1146
|
setResult(null);
|
|
1124
1147
|
setLastError(null);
|
|
1125
1148
|
terminalResultRef.current = null;
|
|
@@ -1132,6 +1155,9 @@ export function VerifyAIScanner({
|
|
|
1132
1155
|
let nativeCaptureAttempts = 0;
|
|
1133
1156
|
let captureRetryAttempted = false;
|
|
1134
1157
|
let captureRetryReady = false;
|
|
1158
|
+
let captureRetryTorchSuppressed = false;
|
|
1159
|
+
let captureRetrySettleDelayMs = CAMERA_CAPTURE_RETRY_DELAY_MS;
|
|
1160
|
+
let captureRetryRemountBackoffMs = 0;
|
|
1135
1161
|
let lastNativeCaptureErrorMessage: string | null = null;
|
|
1136
1162
|
|
|
1137
1163
|
try {
|
|
@@ -1141,6 +1167,8 @@ export function VerifyAIScanner({
|
|
|
1141
1167
|
component: 'scanner',
|
|
1142
1168
|
error: 'capture_orientation_context',
|
|
1143
1169
|
metadata: buildScannerTelemetryMetadata({
|
|
1170
|
+
capture_attempt_id: captureAttemptId,
|
|
1171
|
+
capture_sequence: captureSequence,
|
|
1144
1172
|
capture_physical_orientation: capturePhysicalOrientation,
|
|
1145
1173
|
capture_overlay_rotation_deg: captureOverlayRotationDeg,
|
|
1146
1174
|
capture_rotation_applied: 0,
|
|
@@ -1209,11 +1237,26 @@ export function VerifyAIScanner({
|
|
|
1209
1237
|
|
|
1210
1238
|
if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
|
|
1211
1239
|
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;
|
|
1212
1247
|
const retryAt = new Date().toISOString();
|
|
1213
1248
|
lastCaptureRetryAtRef.current = retryAt;
|
|
1214
1249
|
cameraRemountCountRef.current++;
|
|
1215
|
-
|
|
1216
|
-
|
|
1250
|
+
if (captureRetryTorchSuppressed) {
|
|
1251
|
+
setTorchSuppressedForRetry(true);
|
|
1252
|
+
}
|
|
1253
|
+
requestCameraRemount(
|
|
1254
|
+
'capture_retry',
|
|
1255
|
+
captureRetryRemountBackoffMs > 0
|
|
1256
|
+
? { backoffMs: captureRetryRemountBackoffMs }
|
|
1257
|
+
: undefined,
|
|
1258
|
+
);
|
|
1259
|
+
await sleep(captureRetrySettleDelayMs);
|
|
1217
1260
|
const captureRetryReadyTimeoutMs = Platform.OS === 'android'
|
|
1218
1261
|
? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
|
|
1219
1262
|
: IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
|
|
@@ -1223,11 +1266,16 @@ export function VerifyAIScanner({
|
|
|
1223
1266
|
error: normalized,
|
|
1224
1267
|
errorCode: normalized.code,
|
|
1225
1268
|
metadata: buildScannerTelemetryMetadata({
|
|
1269
|
+
capture_attempt_id: captureAttemptId,
|
|
1270
|
+
capture_sequence: captureSequence,
|
|
1226
1271
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1227
1272
|
capture_retry_attempted: captureRetryAttempted,
|
|
1228
1273
|
capture_retry_ready: captureRetryReady,
|
|
1229
1274
|
capture_retry_delay_ms: CAMERA_CAPTURE_RETRY_DELAY_MS,
|
|
1275
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1276
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1230
1277
|
capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
|
|
1278
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1231
1279
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1232
1280
|
}),
|
|
1233
1281
|
});
|
|
@@ -1321,6 +1369,8 @@ export function VerifyAIScanner({
|
|
|
1321
1369
|
telemetry?.track('image_processed', {
|
|
1322
1370
|
component: 'scanner',
|
|
1323
1371
|
metadata: buildScannerTelemetryMetadata({
|
|
1372
|
+
capture_attempt_id: captureAttemptId,
|
|
1373
|
+
capture_sequence: captureSequence,
|
|
1324
1374
|
original_width: origWidth,
|
|
1325
1375
|
original_height: origHeight,
|
|
1326
1376
|
processed_width: processedWidth,
|
|
@@ -1330,6 +1380,9 @@ export function VerifyAIScanner({
|
|
|
1330
1380
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1331
1381
|
capture_retry_attempted: captureRetryAttempted,
|
|
1332
1382
|
capture_retry_ready: captureRetryReady,
|
|
1383
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1384
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1385
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1333
1386
|
recovered_native_capture_failure: captureRetryAttempted ? 1 : 0,
|
|
1334
1387
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1335
1388
|
}),
|
|
@@ -1404,9 +1457,14 @@ export function VerifyAIScanner({
|
|
|
1404
1457
|
component: 'scanner',
|
|
1405
1458
|
error,
|
|
1406
1459
|
metadata: buildScannerTelemetryMetadata({
|
|
1460
|
+
capture_attempt_id: captureAttemptId,
|
|
1461
|
+
capture_sequence: captureSequence,
|
|
1407
1462
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1408
1463
|
capture_retry_attempted: captureRetryAttempted,
|
|
1409
1464
|
capture_retry_ready: captureRetryReady,
|
|
1465
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1466
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1467
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1410
1468
|
is_native_camera_capture_error: isNativeCameraCaptureError(error),
|
|
1411
1469
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1412
1470
|
}),
|
|
@@ -1417,8 +1475,10 @@ export function VerifyAIScanner({
|
|
|
1417
1475
|
if (!terminalRequestError) {
|
|
1418
1476
|
setTimeout(() => setStatus('idle'), TRANSIENT_ERROR_DISPLAY_MS);
|
|
1419
1477
|
}
|
|
1478
|
+
} finally {
|
|
1479
|
+
setTorchSuppressedForRetry(false);
|
|
1420
1480
|
}
|
|
1421
|
-
}, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg]);
|
|
1481
|
+
}, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg, enableTorch, setTorchSuppressedForRetry]);
|
|
1422
1482
|
|
|
1423
1483
|
// Expose capture to parent via ref
|
|
1424
1484
|
if (captureRef) {
|
|
@@ -1441,7 +1501,7 @@ export function VerifyAIScanner({
|
|
|
1441
1501
|
}
|
|
1442
1502
|
|
|
1443
1503
|
const showBottomCard = status === 'success' || status === 'error';
|
|
1444
|
-
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch;
|
|
1504
|
+
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch && !torchRetrySuppressed;
|
|
1445
1505
|
|
|
1446
1506
|
return (
|
|
1447
1507
|
<View style={[styles.container, style]}>
|
package/src/types/index.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface VerificationListParams {
|
|
|
64
64
|
export interface VerifyOptions {
|
|
65
65
|
/** Idempotency key to prevent duplicate verifications on retry. */
|
|
66
66
|
idempotencyKey?: string;
|
|
67
|
+
/** Optional primitive metadata attached to client-side request telemetry. */
|
|
68
|
+
telemetryMetadata?: Record<string, string | number | boolean | null | undefined>;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export interface QueueItem {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.5.
|
|
1
|
+
export const SDK_VERSION = '2.5.2';
|