@switchlabs/verify-ai-react-native 2.5.1 → 2.5.3
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 +75 -8
- 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 +82 -8
- 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,12 +20,21 @@ 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
|
+
// iOS AVCaptureSession cold start can legitimately exceed 8s on first launch (permission
|
|
27
|
+
// prompt, first-ever session spin-up, thermal/low-power throttling). An 8s watchdog tore
|
|
28
|
+
// down sessions that were seconds from ready and restarted from scratch, which made slow
|
|
29
|
+
// cold starts *worse* and fired camera_preview_timeout in bursts. Match Android at 10s.
|
|
30
|
+
const IOS_CAMERA_STARTUP_WATCHDOG_MS = 10000;
|
|
26
31
|
const ANDROID_CAMERA_STARTUP_WATCHDOG_MS = 10000;
|
|
27
32
|
const CAMERA_STARTUP_SLOW_TELEMETRY_MS = 3000;
|
|
33
|
+
// Base backoff between startup remounts. Each successive remount escalates this (see
|
|
34
|
+
// startupRemountBackoffMs below) so later attempts get progressively more uninterrupted
|
|
35
|
+
// time to initialize the native session instead of thrashing at a fixed interval.
|
|
28
36
|
const CAMERA_STARTUP_REMOUNT_BACKOFF_MS = 350;
|
|
37
|
+
const CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS = 1200;
|
|
29
38
|
const CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS = 5;
|
|
30
39
|
const ANDROID_ORIENTATION_NO_EVENT_TIMEOUT_MS = 3000;
|
|
31
40
|
const ANDROID_ORIENTATION_SAMPLE_TELEMETRY_MS = 3000;
|
|
@@ -76,6 +85,9 @@ function createScannerError(message, code, name) {
|
|
|
76
85
|
function sleep(ms) {
|
|
77
86
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
78
87
|
}
|
|
88
|
+
function createScannerSessionId() {
|
|
89
|
+
return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
90
|
+
}
|
|
79
91
|
function isNativeCameraCaptureError(error) {
|
|
80
92
|
const message = error.message.toLowerCase();
|
|
81
93
|
return (error.code === CAMERA_CAPTURE_ERROR_CODE ||
|
|
@@ -261,7 +273,12 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
261
273
|
const [physicalOrientation, setPhysicalOrientation] = useState('portrait');
|
|
262
274
|
physicalOrientationRef.current = physicalOrientation;
|
|
263
275
|
const overlayRotationDeg = getOverlayRotationDeg(physicalOrientation);
|
|
276
|
+
const scannerSessionIdRef = useRef(createScannerSessionId());
|
|
277
|
+
const captureSequenceRef = useRef(0);
|
|
278
|
+
const torchRetrySuppressedRef = useRef(false);
|
|
279
|
+
const [torchRetrySuppressed, setTorchRetrySuppressed] = useState(false);
|
|
264
280
|
const buildScannerTelemetryMetadata = useCallback((extra = {}) => compactTelemetryMetadata({
|
|
281
|
+
scanner_session_id: scannerSessionIdRef.current,
|
|
265
282
|
policy,
|
|
266
283
|
status,
|
|
267
284
|
camera_ready: cameraReadyRef.current,
|
|
@@ -290,6 +307,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
290
307
|
last_orientation_remount_at: lastOrientationRemountAtRef.current,
|
|
291
308
|
last_capture_retry_at: lastCaptureRetryAtRef.current,
|
|
292
309
|
requested_torch_enabled: enableTorch ? 1 : 0,
|
|
310
|
+
torch_retry_suppressed: torchRetrySuppressedRef.current,
|
|
293
311
|
android_native_orientation_subscription_active: 0,
|
|
294
312
|
android_native_orientation_event_count: 0,
|
|
295
313
|
android_native_orientation_change_count: 0,
|
|
@@ -342,6 +360,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
342
360
|
]);
|
|
343
361
|
telemetryRef.current = telemetry;
|
|
344
362
|
buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
|
|
363
|
+
const setTorchSuppressedForRetry = useCallback((nextSuppressed) => {
|
|
364
|
+
torchRetrySuppressedRef.current = nextSuppressed;
|
|
365
|
+
setTorchRetrySuppressed(nextSuppressed);
|
|
366
|
+
}, []);
|
|
345
367
|
useEffect(() => {
|
|
346
368
|
const reporter = telemetryRef.current;
|
|
347
369
|
if (reporter) {
|
|
@@ -382,7 +404,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
382
404
|
cameraStartupAttemptStartedAtRef.current = Date.now();
|
|
383
405
|
if (opts.log) {
|
|
384
406
|
console.warn(`VerifyAI[${SDK_VERSION}]: remounting camera reason=${reason} ` +
|
|
385
|
-
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0}`
|
|
407
|
+
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0} ` +
|
|
408
|
+
`torchRetrySuppressed=${torchRetrySuppressedRef.current ? 1 : 0}`);
|
|
386
409
|
}
|
|
387
410
|
const remount = () => {
|
|
388
411
|
setCameraKey((k) => k + 1);
|
|
@@ -829,6 +852,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
829
852
|
const startupWatchdogRemountCount = startupWatchdogRemountCountRef.current + 1;
|
|
830
853
|
startupWatchdogRemountCountRef.current = startupWatchdogRemountCount;
|
|
831
854
|
cameraRemountCountRef.current++;
|
|
855
|
+
// Escalate the backoff with each attempt (350, 700, 1050, capped at 1200ms) so a
|
|
856
|
+
// camera that just needs more time isn't repeatedly interrupted at a fixed cadence.
|
|
857
|
+
const startupRemountBackoffMs = Math.min(CAMERA_STARTUP_REMOUNT_BACKOFF_MS * startupWatchdogRemountCount, CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS);
|
|
832
858
|
telemetry?.track('camera_preview_timeout', {
|
|
833
859
|
component: 'scanner',
|
|
834
860
|
error: `Camera did not initialize within ${Math.round(watchdogMs / 1000)} seconds — remounting`,
|
|
@@ -837,12 +863,12 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
837
863
|
remount_reason: 'startup_watchdog',
|
|
838
864
|
startup_watchdog_remount_count: startupWatchdogRemountCount,
|
|
839
865
|
startup_watchdog_max_remounts: CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS,
|
|
840
|
-
startup_remount_backoff_ms:
|
|
866
|
+
startup_remount_backoff_ms: startupRemountBackoffMs,
|
|
841
867
|
torch_deferred_until_ready: enableTorch ? 1 : 0,
|
|
842
868
|
}),
|
|
843
869
|
});
|
|
844
870
|
requestCameraRemount('startup_watchdog', {
|
|
845
|
-
backoffMs:
|
|
871
|
+
backoffMs: startupRemountBackoffMs,
|
|
846
872
|
log: true,
|
|
847
873
|
});
|
|
848
874
|
}
|
|
@@ -875,6 +901,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
875
901
|
}
|
|
876
902
|
}, [buildScannerTelemetryMetadata, permission, telemetry]);
|
|
877
903
|
const handleCapture = useCallback(async () => {
|
|
904
|
+
const captureSequence = captureSequenceRef.current + 1;
|
|
905
|
+
captureSequenceRef.current = captureSequence;
|
|
906
|
+
const captureAttemptId = `${scannerSessionIdRef.current}_cap_${captureSequence}`;
|
|
878
907
|
const blockedReason = !cameraRef.current
|
|
879
908
|
? 'camera_ref_null'
|
|
880
909
|
: !cameraReadyRef.current
|
|
@@ -892,6 +921,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
892
921
|
component: 'scanner',
|
|
893
922
|
error: blockedReason == null ? 'capture_requested' : 'capture_ignored',
|
|
894
923
|
metadata: buildScannerTelemetryMetadata({
|
|
924
|
+
capture_attempt_id: captureAttemptId,
|
|
925
|
+
capture_sequence: captureSequence,
|
|
895
926
|
capture_blocked_reason: blockedReason,
|
|
896
927
|
}),
|
|
897
928
|
});
|
|
@@ -912,6 +943,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
912
943
|
return;
|
|
913
944
|
}
|
|
914
945
|
setStatus('capturing');
|
|
946
|
+
setTorchSuppressedForRetry(false);
|
|
915
947
|
setResult(null);
|
|
916
948
|
setLastError(null);
|
|
917
949
|
terminalResultRef.current = null;
|
|
@@ -923,6 +955,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
923
955
|
let nativeCaptureAttempts = 0;
|
|
924
956
|
let captureRetryAttempted = false;
|
|
925
957
|
let captureRetryReady = false;
|
|
958
|
+
let captureRetryTorchSuppressed = false;
|
|
959
|
+
let captureRetrySettleDelayMs = CAMERA_CAPTURE_RETRY_DELAY_MS;
|
|
960
|
+
let captureRetryRemountBackoffMs = 0;
|
|
926
961
|
let lastNativeCaptureErrorMessage = null;
|
|
927
962
|
try {
|
|
928
963
|
const capturePhysicalOrientation = physicalOrientation;
|
|
@@ -931,6 +966,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
931
966
|
component: 'scanner',
|
|
932
967
|
error: 'capture_orientation_context',
|
|
933
968
|
metadata: buildScannerTelemetryMetadata({
|
|
969
|
+
capture_attempt_id: captureAttemptId,
|
|
970
|
+
capture_sequence: captureSequence,
|
|
934
971
|
capture_physical_orientation: capturePhysicalOrientation,
|
|
935
972
|
capture_overlay_rotation_deg: captureOverlayRotationDeg,
|
|
936
973
|
capture_rotation_applied: 0,
|
|
@@ -988,11 +1025,23 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
988
1025
|
lastNativeCaptureErrorMessage = normalized.message;
|
|
989
1026
|
if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
|
|
990
1027
|
captureRetryAttempted = true;
|
|
1028
|
+
captureRetryTorchSuppressed = !!enableTorch;
|
|
1029
|
+
captureRetrySettleDelayMs = captureRetryTorchSuppressed
|
|
1030
|
+
? CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS
|
|
1031
|
+
: CAMERA_CAPTURE_RETRY_DELAY_MS;
|
|
1032
|
+
captureRetryRemountBackoffMs = captureRetryTorchSuppressed
|
|
1033
|
+
? CAMERA_CAPTURE_RETRY_DELAY_MS
|
|
1034
|
+
: 0;
|
|
991
1035
|
const retryAt = new Date().toISOString();
|
|
992
1036
|
lastCaptureRetryAtRef.current = retryAt;
|
|
993
1037
|
cameraRemountCountRef.current++;
|
|
994
|
-
|
|
995
|
-
|
|
1038
|
+
if (captureRetryTorchSuppressed) {
|
|
1039
|
+
setTorchSuppressedForRetry(true);
|
|
1040
|
+
}
|
|
1041
|
+
requestCameraRemount('capture_retry', captureRetryRemountBackoffMs > 0
|
|
1042
|
+
? { backoffMs: captureRetryRemountBackoffMs }
|
|
1043
|
+
: undefined);
|
|
1044
|
+
await sleep(captureRetrySettleDelayMs);
|
|
996
1045
|
const captureRetryReadyTimeoutMs = Platform.OS === 'android'
|
|
997
1046
|
? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
|
|
998
1047
|
: IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
|
|
@@ -1002,11 +1051,16 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1002
1051
|
error: normalized,
|
|
1003
1052
|
errorCode: normalized.code,
|
|
1004
1053
|
metadata: buildScannerTelemetryMetadata({
|
|
1054
|
+
capture_attempt_id: captureAttemptId,
|
|
1055
|
+
capture_sequence: captureSequence,
|
|
1005
1056
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1006
1057
|
capture_retry_attempted: captureRetryAttempted,
|
|
1007
1058
|
capture_retry_ready: captureRetryReady,
|
|
1008
1059
|
capture_retry_delay_ms: CAMERA_CAPTURE_RETRY_DELAY_MS,
|
|
1060
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1061
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1009
1062
|
capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
|
|
1063
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1010
1064
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1011
1065
|
}),
|
|
1012
1066
|
});
|
|
@@ -1074,6 +1128,8 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1074
1128
|
telemetry?.track('image_processed', {
|
|
1075
1129
|
component: 'scanner',
|
|
1076
1130
|
metadata: buildScannerTelemetryMetadata({
|
|
1131
|
+
capture_attempt_id: captureAttemptId,
|
|
1132
|
+
capture_sequence: captureSequence,
|
|
1077
1133
|
original_width: origWidth,
|
|
1078
1134
|
original_height: origHeight,
|
|
1079
1135
|
processed_width: processedWidth,
|
|
@@ -1083,6 +1139,9 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1083
1139
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1084
1140
|
capture_retry_attempted: captureRetryAttempted,
|
|
1085
1141
|
capture_retry_ready: captureRetryReady,
|
|
1142
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1143
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1144
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1086
1145
|
recovered_native_capture_failure: captureRetryAttempted ? 1 : 0,
|
|
1087
1146
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1088
1147
|
}),
|
|
@@ -1151,9 +1210,14 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1151
1210
|
component: 'scanner',
|
|
1152
1211
|
error,
|
|
1153
1212
|
metadata: buildScannerTelemetryMetadata({
|
|
1213
|
+
capture_attempt_id: captureAttemptId,
|
|
1214
|
+
capture_sequence: captureSequence,
|
|
1154
1215
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1155
1216
|
capture_retry_attempted: captureRetryAttempted,
|
|
1156
1217
|
capture_retry_ready: captureRetryReady,
|
|
1218
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1219
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1220
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1157
1221
|
is_native_camera_capture_error: isNativeCameraCaptureError(error),
|
|
1158
1222
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1159
1223
|
}),
|
|
@@ -1163,7 +1227,10 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1163
1227
|
setTimeout(() => setStatus('idle'), TRANSIENT_ERROR_DISPLAY_MS);
|
|
1164
1228
|
}
|
|
1165
1229
|
}
|
|
1166
|
-
|
|
1230
|
+
finally {
|
|
1231
|
+
setTorchSuppressedForRetry(false);
|
|
1232
|
+
}
|
|
1233
|
+
}, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg, enableTorch, setTorchSuppressedForRetry]);
|
|
1167
1234
|
// Expose capture to parent via ref
|
|
1168
1235
|
if (captureRef) {
|
|
1169
1236
|
captureRef.current = handleCapture;
|
|
@@ -1175,7 +1242,7 @@ export function VerifyAIScanner({ onCapture, policy, onResult, onError, onClose,
|
|
|
1175
1242
|
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
1243
|
}
|
|
1177
1244
|
const showBottomCard = status === 'success' || status === 'error';
|
|
1178
|
-
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch;
|
|
1245
|
+
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch && !torchRetrySuppressed;
|
|
1179
1246
|
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
1247
|
setPhysicalOrientation(event.orientation);
|
|
1181
1248
|
}, 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,12 +45,21 @@ 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
|
+
// iOS AVCaptureSession cold start can legitimately exceed 8s on first launch (permission
|
|
52
|
+
// prompt, first-ever session spin-up, thermal/low-power throttling). An 8s watchdog tore
|
|
53
|
+
// down sessions that were seconds from ready and restarted from scratch, which made slow
|
|
54
|
+
// cold starts *worse* and fired camera_preview_timeout in bursts. Match Android at 10s.
|
|
55
|
+
const IOS_CAMERA_STARTUP_WATCHDOG_MS = 10000;
|
|
51
56
|
const ANDROID_CAMERA_STARTUP_WATCHDOG_MS = 10000;
|
|
52
57
|
const CAMERA_STARTUP_SLOW_TELEMETRY_MS = 3000;
|
|
58
|
+
// Base backoff between startup remounts. Each successive remount escalates this (see
|
|
59
|
+
// startupRemountBackoffMs below) so later attempts get progressively more uninterrupted
|
|
60
|
+
// time to initialize the native session instead of thrashing at a fixed interval.
|
|
53
61
|
const CAMERA_STARTUP_REMOUNT_BACKOFF_MS = 350;
|
|
62
|
+
const CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS = 1200;
|
|
54
63
|
const CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS = 5;
|
|
55
64
|
const ANDROID_ORIENTATION_NO_EVENT_TIMEOUT_MS = 3000;
|
|
56
65
|
const ANDROID_ORIENTATION_SAMPLE_TELEMETRY_MS = 3000;
|
|
@@ -153,6 +162,10 @@ function sleep(ms: number): Promise<void> {
|
|
|
153
162
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
function createScannerSessionId(): string {
|
|
166
|
+
return `scan_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
156
169
|
function isNativeCameraCaptureError(error: ErrorWithDetails): boolean {
|
|
157
170
|
const message = error.message.toLowerCase();
|
|
158
171
|
return (
|
|
@@ -369,10 +382,15 @@ export function VerifyAIScanner({
|
|
|
369
382
|
|
|
370
383
|
physicalOrientationRef.current = physicalOrientation;
|
|
371
384
|
const overlayRotationDeg = getOverlayRotationDeg(physicalOrientation);
|
|
385
|
+
const scannerSessionIdRef = useRef(createScannerSessionId());
|
|
386
|
+
const captureSequenceRef = useRef(0);
|
|
387
|
+
const torchRetrySuppressedRef = useRef(false);
|
|
388
|
+
const [torchRetrySuppressed, setTorchRetrySuppressed] = useState(false);
|
|
372
389
|
|
|
373
390
|
const buildScannerTelemetryMetadata = useCallback((
|
|
374
391
|
extra: Record<string, string | number | boolean | null | undefined> = {},
|
|
375
392
|
): ScannerTelemetryMetadata => compactTelemetryMetadata({
|
|
393
|
+
scanner_session_id: scannerSessionIdRef.current,
|
|
376
394
|
policy,
|
|
377
395
|
status,
|
|
378
396
|
camera_ready: cameraReadyRef.current,
|
|
@@ -401,6 +419,7 @@ export function VerifyAIScanner({
|
|
|
401
419
|
last_orientation_remount_at: lastOrientationRemountAtRef.current,
|
|
402
420
|
last_capture_retry_at: lastCaptureRetryAtRef.current,
|
|
403
421
|
requested_torch_enabled: enableTorch ? 1 : 0,
|
|
422
|
+
torch_retry_suppressed: torchRetrySuppressedRef.current,
|
|
404
423
|
android_native_orientation_subscription_active: 0,
|
|
405
424
|
android_native_orientation_event_count: 0,
|
|
406
425
|
android_native_orientation_change_count: 0,
|
|
@@ -455,6 +474,11 @@ export function VerifyAIScanner({
|
|
|
455
474
|
telemetryRef.current = telemetry;
|
|
456
475
|
buildScannerTelemetryMetadataRef.current = buildScannerTelemetryMetadata;
|
|
457
476
|
|
|
477
|
+
const setTorchSuppressedForRetry = useCallback((nextSuppressed: boolean) => {
|
|
478
|
+
torchRetrySuppressedRef.current = nextSuppressed;
|
|
479
|
+
setTorchRetrySuppressed(nextSuppressed);
|
|
480
|
+
}, []);
|
|
481
|
+
|
|
458
482
|
useEffect(() => {
|
|
459
483
|
const reporter = telemetryRef.current;
|
|
460
484
|
if (reporter) {
|
|
@@ -509,7 +533,8 @@ export function VerifyAIScanner({
|
|
|
509
533
|
if (opts.log) {
|
|
510
534
|
console.warn(
|
|
511
535
|
`VerifyAI[${SDK_VERSION}]: remounting camera reason=${reason} ` +
|
|
512
|
-
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0}
|
|
536
|
+
`backoffMs=${opts.backoffMs ?? 0} torchRequested=${enableTorch ? 1 : 0} ` +
|
|
537
|
+
`torchRetrySuppressed=${torchRetrySuppressedRef.current ? 1 : 0}`,
|
|
513
538
|
);
|
|
514
539
|
}
|
|
515
540
|
|
|
@@ -1025,6 +1050,12 @@ export function VerifyAIScanner({
|
|
|
1025
1050
|
const startupWatchdogRemountCount = startupWatchdogRemountCountRef.current + 1;
|
|
1026
1051
|
startupWatchdogRemountCountRef.current = startupWatchdogRemountCount;
|
|
1027
1052
|
cameraRemountCountRef.current++;
|
|
1053
|
+
// Escalate the backoff with each attempt (350, 700, 1050, capped at 1200ms) so a
|
|
1054
|
+
// camera that just needs more time isn't repeatedly interrupted at a fixed cadence.
|
|
1055
|
+
const startupRemountBackoffMs = Math.min(
|
|
1056
|
+
CAMERA_STARTUP_REMOUNT_BACKOFF_MS * startupWatchdogRemountCount,
|
|
1057
|
+
CAMERA_STARTUP_REMOUNT_BACKOFF_MAX_MS,
|
|
1058
|
+
);
|
|
1028
1059
|
telemetry?.track('camera_preview_timeout', {
|
|
1029
1060
|
component: 'scanner',
|
|
1030
1061
|
error: `Camera did not initialize within ${Math.round(watchdogMs / 1000)} seconds — remounting`,
|
|
@@ -1033,12 +1064,12 @@ export function VerifyAIScanner({
|
|
|
1033
1064
|
remount_reason: 'startup_watchdog',
|
|
1034
1065
|
startup_watchdog_remount_count: startupWatchdogRemountCount,
|
|
1035
1066
|
startup_watchdog_max_remounts: CAMERA_STARTUP_WATCHDOG_MAX_REMOUNTS,
|
|
1036
|
-
startup_remount_backoff_ms:
|
|
1067
|
+
startup_remount_backoff_ms: startupRemountBackoffMs,
|
|
1037
1068
|
torch_deferred_until_ready: enableTorch ? 1 : 0,
|
|
1038
1069
|
}),
|
|
1039
1070
|
});
|
|
1040
1071
|
requestCameraRemount('startup_watchdog', {
|
|
1041
|
-
backoffMs:
|
|
1072
|
+
backoffMs: startupRemountBackoffMs,
|
|
1042
1073
|
log: true,
|
|
1043
1074
|
});
|
|
1044
1075
|
}
|
|
@@ -1076,6 +1107,9 @@ export function VerifyAIScanner({
|
|
|
1076
1107
|
}, [buildScannerTelemetryMetadata, permission, telemetry]);
|
|
1077
1108
|
|
|
1078
1109
|
const handleCapture = useCallback(async () => {
|
|
1110
|
+
const captureSequence = captureSequenceRef.current + 1;
|
|
1111
|
+
captureSequenceRef.current = captureSequence;
|
|
1112
|
+
const captureAttemptId = `${scannerSessionIdRef.current}_cap_${captureSequence}`;
|
|
1079
1113
|
const blockedReason =
|
|
1080
1114
|
!cameraRef.current
|
|
1081
1115
|
? 'camera_ref_null'
|
|
@@ -1095,6 +1129,8 @@ export function VerifyAIScanner({
|
|
|
1095
1129
|
component: 'scanner',
|
|
1096
1130
|
error: blockedReason == null ? 'capture_requested' : 'capture_ignored',
|
|
1097
1131
|
metadata: buildScannerTelemetryMetadata({
|
|
1132
|
+
capture_attempt_id: captureAttemptId,
|
|
1133
|
+
capture_sequence: captureSequence,
|
|
1098
1134
|
capture_blocked_reason: blockedReason,
|
|
1099
1135
|
}),
|
|
1100
1136
|
});
|
|
@@ -1120,6 +1156,7 @@ export function VerifyAIScanner({
|
|
|
1120
1156
|
}
|
|
1121
1157
|
|
|
1122
1158
|
setStatus('capturing');
|
|
1159
|
+
setTorchSuppressedForRetry(false);
|
|
1123
1160
|
setResult(null);
|
|
1124
1161
|
setLastError(null);
|
|
1125
1162
|
terminalResultRef.current = null;
|
|
@@ -1132,6 +1169,9 @@ export function VerifyAIScanner({
|
|
|
1132
1169
|
let nativeCaptureAttempts = 0;
|
|
1133
1170
|
let captureRetryAttempted = false;
|
|
1134
1171
|
let captureRetryReady = false;
|
|
1172
|
+
let captureRetryTorchSuppressed = false;
|
|
1173
|
+
let captureRetrySettleDelayMs = CAMERA_CAPTURE_RETRY_DELAY_MS;
|
|
1174
|
+
let captureRetryRemountBackoffMs = 0;
|
|
1135
1175
|
let lastNativeCaptureErrorMessage: string | null = null;
|
|
1136
1176
|
|
|
1137
1177
|
try {
|
|
@@ -1141,6 +1181,8 @@ export function VerifyAIScanner({
|
|
|
1141
1181
|
component: 'scanner',
|
|
1142
1182
|
error: 'capture_orientation_context',
|
|
1143
1183
|
metadata: buildScannerTelemetryMetadata({
|
|
1184
|
+
capture_attempt_id: captureAttemptId,
|
|
1185
|
+
capture_sequence: captureSequence,
|
|
1144
1186
|
capture_physical_orientation: capturePhysicalOrientation,
|
|
1145
1187
|
capture_overlay_rotation_deg: captureOverlayRotationDeg,
|
|
1146
1188
|
capture_rotation_applied: 0,
|
|
@@ -1209,11 +1251,26 @@ export function VerifyAIScanner({
|
|
|
1209
1251
|
|
|
1210
1252
|
if (attempt === 1 && isNativeCameraCaptureError(normalized) && !terminated) {
|
|
1211
1253
|
captureRetryAttempted = true;
|
|
1254
|
+
captureRetryTorchSuppressed = !!enableTorch;
|
|
1255
|
+
captureRetrySettleDelayMs = captureRetryTorchSuppressed
|
|
1256
|
+
? CAMERA_CAPTURE_RETRY_TORCH_SETTLE_MS
|
|
1257
|
+
: CAMERA_CAPTURE_RETRY_DELAY_MS;
|
|
1258
|
+
captureRetryRemountBackoffMs = captureRetryTorchSuppressed
|
|
1259
|
+
? CAMERA_CAPTURE_RETRY_DELAY_MS
|
|
1260
|
+
: 0;
|
|
1212
1261
|
const retryAt = new Date().toISOString();
|
|
1213
1262
|
lastCaptureRetryAtRef.current = retryAt;
|
|
1214
1263
|
cameraRemountCountRef.current++;
|
|
1215
|
-
|
|
1216
|
-
|
|
1264
|
+
if (captureRetryTorchSuppressed) {
|
|
1265
|
+
setTorchSuppressedForRetry(true);
|
|
1266
|
+
}
|
|
1267
|
+
requestCameraRemount(
|
|
1268
|
+
'capture_retry',
|
|
1269
|
+
captureRetryRemountBackoffMs > 0
|
|
1270
|
+
? { backoffMs: captureRetryRemountBackoffMs }
|
|
1271
|
+
: undefined,
|
|
1272
|
+
);
|
|
1273
|
+
await sleep(captureRetrySettleDelayMs);
|
|
1217
1274
|
const captureRetryReadyTimeoutMs = Platform.OS === 'android'
|
|
1218
1275
|
? ANDROID_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS
|
|
1219
1276
|
: IOS_CAMERA_CAPTURE_RETRY_READY_TIMEOUT_MS;
|
|
@@ -1223,11 +1280,16 @@ export function VerifyAIScanner({
|
|
|
1223
1280
|
error: normalized,
|
|
1224
1281
|
errorCode: normalized.code,
|
|
1225
1282
|
metadata: buildScannerTelemetryMetadata({
|
|
1283
|
+
capture_attempt_id: captureAttemptId,
|
|
1284
|
+
capture_sequence: captureSequence,
|
|
1226
1285
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1227
1286
|
capture_retry_attempted: captureRetryAttempted,
|
|
1228
1287
|
capture_retry_ready: captureRetryReady,
|
|
1229
1288
|
capture_retry_delay_ms: CAMERA_CAPTURE_RETRY_DELAY_MS,
|
|
1289
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1290
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1230
1291
|
capture_retry_ready_timeout_ms: captureRetryReadyTimeoutMs,
|
|
1292
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1231
1293
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1232
1294
|
}),
|
|
1233
1295
|
});
|
|
@@ -1321,6 +1383,8 @@ export function VerifyAIScanner({
|
|
|
1321
1383
|
telemetry?.track('image_processed', {
|
|
1322
1384
|
component: 'scanner',
|
|
1323
1385
|
metadata: buildScannerTelemetryMetadata({
|
|
1386
|
+
capture_attempt_id: captureAttemptId,
|
|
1387
|
+
capture_sequence: captureSequence,
|
|
1324
1388
|
original_width: origWidth,
|
|
1325
1389
|
original_height: origHeight,
|
|
1326
1390
|
processed_width: processedWidth,
|
|
@@ -1330,6 +1394,9 @@ export function VerifyAIScanner({
|
|
|
1330
1394
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1331
1395
|
capture_retry_attempted: captureRetryAttempted,
|
|
1332
1396
|
capture_retry_ready: captureRetryReady,
|
|
1397
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1398
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1399
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1333
1400
|
recovered_native_capture_failure: captureRetryAttempted ? 1 : 0,
|
|
1334
1401
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1335
1402
|
}),
|
|
@@ -1404,9 +1471,14 @@ export function VerifyAIScanner({
|
|
|
1404
1471
|
component: 'scanner',
|
|
1405
1472
|
error,
|
|
1406
1473
|
metadata: buildScannerTelemetryMetadata({
|
|
1474
|
+
capture_attempt_id: captureAttemptId,
|
|
1475
|
+
capture_sequence: captureSequence,
|
|
1407
1476
|
native_capture_attempts: nativeCaptureAttempts,
|
|
1408
1477
|
capture_retry_attempted: captureRetryAttempted,
|
|
1409
1478
|
capture_retry_ready: captureRetryReady,
|
|
1479
|
+
capture_retry_torch_suppressed: captureRetryTorchSuppressed,
|
|
1480
|
+
capture_retry_settle_delay_ms: captureRetrySettleDelayMs,
|
|
1481
|
+
capture_retry_remount_backoff_ms: captureRetryRemountBackoffMs,
|
|
1410
1482
|
is_native_camera_capture_error: isNativeCameraCaptureError(error),
|
|
1411
1483
|
last_native_capture_error: lastNativeCaptureErrorMessage,
|
|
1412
1484
|
}),
|
|
@@ -1417,8 +1489,10 @@ export function VerifyAIScanner({
|
|
|
1417
1489
|
if (!terminalRequestError) {
|
|
1418
1490
|
setTimeout(() => setStatus('idle'), TRANSIENT_ERROR_DISPLAY_MS);
|
|
1419
1491
|
}
|
|
1492
|
+
} finally {
|
|
1493
|
+
setTorchSuppressedForRetry(false);
|
|
1420
1494
|
}
|
|
1421
|
-
}, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg]);
|
|
1495
|
+
}, [status, exhausted, onCapture, onError, overlay?.maxAttempts, overlay?.autoApproveOnExhaust, releaseCamera, scheduleTerminalResult, buildScannerTelemetryMetadata, requestCameraRemount, telemetry, terminated, physicalOrientation, overlayRotationDeg, enableTorch, setTorchSuppressedForRetry]);
|
|
1422
1496
|
|
|
1423
1497
|
// Expose capture to parent via ref
|
|
1424
1498
|
if (captureRef) {
|
|
@@ -1441,7 +1515,7 @@ export function VerifyAIScanner({
|
|
|
1441
1515
|
}
|
|
1442
1516
|
|
|
1443
1517
|
const showBottomCard = status === 'success' || status === 'error';
|
|
1444
|
-
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch;
|
|
1518
|
+
const shouldEnableTorch = !terminated && cameraReady && !!enableTorch && !torchRetrySuppressed;
|
|
1445
1519
|
|
|
1446
1520
|
return (
|
|
1447
1521
|
<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';
|