react-native-tediwave-biometric-verifier 0.1.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/LICENSE +9 -0
- package/README.md +85 -0
- package/TediwaveBiometricVerifier.podspec +20 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/tediwavebiometricverifier/TediwaveBiometricVerifierModule.kt +15 -0
- package/android/src/main/java/com/tediwavebiometricverifier/TediwaveBiometricVerifierPackage.kt +31 -0
- package/ios/TediwaveBiometricVerifier.h +5 -0
- package/ios/TediwaveBiometricVerifier.mm +21 -0
- package/lib/module/BiometricCamera.js +30 -0
- package/lib/module/BiometricCamera.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/logger.js +38 -0
- package/lib/module/logger.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/useBiometricEngine.js +458 -0
- package/lib/module/useBiometricEngine.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/BiometricCamera.d.ts +14 -0
- package/lib/typescript/src/BiometricCamera.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/logger.d.ts +10 -0
- package/lib/typescript/src/logger.d.ts.map +1 -0
- package/lib/typescript/src/useBiometricEngine.d.ts +99 -0
- package/lib/typescript/src/useBiometricEngine.d.ts.map +1 -0
- package/package.json +159 -0
- package/src/BiometricCamera.tsx +39 -0
- package/src/index.tsx +6 -0
- package/src/logger.ts +46 -0
- package/src/useBiometricEngine.ts +626 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { type Frame } from 'react-native-vision-camera';
|
|
3
|
+
import { useResizePlugin } from 'vision-camera-resize-plugin';
|
|
4
|
+
import RNTts from 'react-native-tts';
|
|
5
|
+
import { logger } from './logger';
|
|
6
|
+
|
|
7
|
+
// ==================== TYPE DEFINITIONS ====================
|
|
8
|
+
|
|
9
|
+
export type VerificationStatus =
|
|
10
|
+
| 'INITIALIZING'
|
|
11
|
+
| 'FACE_DETECTION'
|
|
12
|
+
| 'WAITING_FOR_FACE'
|
|
13
|
+
| 'FACE_READY'
|
|
14
|
+
| 'CHALLENGE_IN_PROGRESS'
|
|
15
|
+
| 'VERIFYING'
|
|
16
|
+
| 'PAUSED'
|
|
17
|
+
| 'COMPLETED';
|
|
18
|
+
|
|
19
|
+
export type VerificationResult = 'PASS' | 'FAIL' | 'TIMEOUT' | 'ERROR';
|
|
20
|
+
|
|
21
|
+
export type ChallengeType =
|
|
22
|
+
| 'FACE_THE_CAMERA'
|
|
23
|
+
| 'BLINK_ONCE'
|
|
24
|
+
| 'BLINK_TWICE'
|
|
25
|
+
| 'SMILE'
|
|
26
|
+
| 'TURN_HEAD_LEFT'
|
|
27
|
+
| 'TURN_HEAD_RIGHT'
|
|
28
|
+
| 'LOOK_UP'
|
|
29
|
+
| 'LOOK_DOWN'
|
|
30
|
+
| 'RAISE_EYEBROWS'
|
|
31
|
+
| 'NOD_HEAD'
|
|
32
|
+
| 'SHAKE_HEAD'
|
|
33
|
+
| 'OPEN_MOUTH'
|
|
34
|
+
| 'CLOSE_EYES_BRIEFLY';
|
|
35
|
+
|
|
36
|
+
export interface ChallengeResult {
|
|
37
|
+
challenge: ChallengeType;
|
|
38
|
+
status: 'PASS' | 'FAIL' | 'TIMEOUT';
|
|
39
|
+
timestamp: number;
|
|
40
|
+
duration: number;
|
|
41
|
+
confidence: number;
|
|
42
|
+
failureReason?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FaceQualityMetrics {
|
|
46
|
+
isDetected: boolean;
|
|
47
|
+
faceCount: number;
|
|
48
|
+
isCentered: boolean;
|
|
49
|
+
isProperlyLit: boolean;
|
|
50
|
+
faceSize: number;
|
|
51
|
+
sharpness: number;
|
|
52
|
+
angularDeviation: number;
|
|
53
|
+
occlusion: number;
|
|
54
|
+
quality: number; // 0-100
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface VerificationPayload {
|
|
58
|
+
status: VerificationResult;
|
|
59
|
+
liveImage: string;
|
|
60
|
+
referenceImage?: string;
|
|
61
|
+
livenessScore: number;
|
|
62
|
+
faceMatchScore?: number;
|
|
63
|
+
challengeResults: ChallengeResult[];
|
|
64
|
+
timestamp: number;
|
|
65
|
+
sessionDuration: number;
|
|
66
|
+
antiSpoofingChecks: Record<string, boolean>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type BiometricMode = 'LIVENESS' | 'IDENTITY';
|
|
70
|
+
|
|
71
|
+
export interface VoiceOptions {
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
language?: string;
|
|
74
|
+
rate?: number; // 0..1
|
|
75
|
+
volume?: number; // 0..1 (best-effort; platform dependent)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface BiometricEngineConfig {
|
|
79
|
+
apiKey: string;
|
|
80
|
+
referenceImage?: string;
|
|
81
|
+
mode?: BiometricMode;
|
|
82
|
+
voiceOptions?: VoiceOptions;
|
|
83
|
+
autoStart?: boolean;
|
|
84
|
+
challengeTimeout?: number;
|
|
85
|
+
sessionTimeout?: number;
|
|
86
|
+
onResult?: (payload: VerificationPayload) => void;
|
|
87
|
+
onStatusChange?: (status: VerificationStatus) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ==================== CHALLENGE DEFINITIONS ====================
|
|
91
|
+
|
|
92
|
+
const CHALLENGE_INSTRUCTIONS: Record<ChallengeType, string> = {
|
|
93
|
+
FACE_THE_CAMERA: 'Please face the camera directly',
|
|
94
|
+
BLINK_ONCE: 'Blink once',
|
|
95
|
+
BLINK_TWICE: 'Blink twice',
|
|
96
|
+
SMILE: 'Smile for the camera',
|
|
97
|
+
TURN_HEAD_LEFT: 'Turn your head to the left',
|
|
98
|
+
TURN_HEAD_RIGHT: 'Turn your head to the right',
|
|
99
|
+
LOOK_UP: 'Look up',
|
|
100
|
+
LOOK_DOWN: 'Look down',
|
|
101
|
+
RAISE_EYEBROWS: 'Raise your eyebrows',
|
|
102
|
+
NOD_HEAD: 'Nod your head',
|
|
103
|
+
SHAKE_HEAD: 'Shake your head left and right',
|
|
104
|
+
OPEN_MOUTH: 'Open your mouth',
|
|
105
|
+
CLOSE_EYES_BRIEFLY: 'Close your eyes briefly',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const OPTIONAL_CHALLENGES: ChallengeType[] = [
|
|
109
|
+
'BLINK_ONCE',
|
|
110
|
+
'BLINK_TWICE',
|
|
111
|
+
'SMILE',
|
|
112
|
+
'TURN_HEAD_LEFT',
|
|
113
|
+
'TURN_HEAD_RIGHT',
|
|
114
|
+
'LOOK_UP',
|
|
115
|
+
'LOOK_DOWN',
|
|
116
|
+
'RAISE_EYEBROWS',
|
|
117
|
+
'NOD_HEAD',
|
|
118
|
+
'SHAKE_HEAD',
|
|
119
|
+
'OPEN_MOUTH',
|
|
120
|
+
'CLOSE_EYES_BRIEFLY',
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// ==================== VERIFICATION SERVICE LAYER ====================
|
|
124
|
+
|
|
125
|
+
class VerificationService {
|
|
126
|
+
private apiKey: string;
|
|
127
|
+
|
|
128
|
+
constructor(apiKey: string) {
|
|
129
|
+
this.apiKey = apiKey;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async verifyLiveness(
|
|
133
|
+
_liveImage: string,
|
|
134
|
+
challengeResults: ChallengeResult[]
|
|
135
|
+
): Promise<{ score: number; passed: boolean }> {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
const passedChallenges = challengeResults.filter(
|
|
139
|
+
(r) => r.status === 'PASS'
|
|
140
|
+
).length;
|
|
141
|
+
const score =
|
|
142
|
+
(passedChallenges / Math.max(1, challengeResults.length)) * 100;
|
|
143
|
+
resolve({
|
|
144
|
+
score: Math.round(score),
|
|
145
|
+
passed: passedChallenges >= 2,
|
|
146
|
+
});
|
|
147
|
+
}, 500);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async verifyIdentity(
|
|
152
|
+
_liveImage: string,
|
|
153
|
+
_referenceImage: string
|
|
154
|
+
): Promise<{ score: number; passed: boolean }> {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
const score = Math.random() * 100;
|
|
158
|
+
resolve({ score: Math.round(score), passed: score > 70 });
|
|
159
|
+
}, 500);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getApiKey(): string {
|
|
164
|
+
return this.apiKey;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ==================== UTILITY FUNCTIONS ====================
|
|
169
|
+
|
|
170
|
+
function getRandomChallenges(count: number): ChallengeType[] {
|
|
171
|
+
const shuffled = [...OPTIONAL_CHALLENGES].sort(() => Math.random() - 0.5);
|
|
172
|
+
return shuffled.slice(0, count);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildChallengeSequence(): ChallengeType[] {
|
|
176
|
+
return ['FACE_THE_CAMERA', ...getRandomChallenges(2)];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function speakGuidance(
|
|
180
|
+
text: string,
|
|
181
|
+
opts: VoiceOptions = { enabled: true, rate: 0.85 }
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
if (!opts?.enabled) return;
|
|
184
|
+
try {
|
|
185
|
+
if (opts.language) {
|
|
186
|
+
// best-effort; may not exist on all platforms
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
await RNTts.setDefaultLanguage?.(opts.language);
|
|
189
|
+
}
|
|
190
|
+
if (typeof opts.rate === 'number') {
|
|
191
|
+
await RNTts.setDefaultRate?.(opts.rate);
|
|
192
|
+
}
|
|
193
|
+
await RNTts.speak(text);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logger.debug('BiometricEngine', { ttsError: error });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ==================== MOCK OPENCV ====================
|
|
200
|
+
|
|
201
|
+
const OpenCV = {
|
|
202
|
+
frameBufferToMat: (
|
|
203
|
+
_height: number,
|
|
204
|
+
_width: number,
|
|
205
|
+
_channels: number,
|
|
206
|
+
_buffer: unknown
|
|
207
|
+
) => ({}),
|
|
208
|
+
createObject: () => ({}),
|
|
209
|
+
invoke: (..._args: unknown[]) => {},
|
|
210
|
+
toJSValue: (_mat: unknown) => ({ rows: 0, cols: 0 }),
|
|
211
|
+
clearBuffers: () => {},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// ==================== MAIN BIOMETRIC ENGINE HOOK (HEADLESS) ====================
|
|
215
|
+
|
|
216
|
+
export function useBiometricEngine(config: BiometricEngineConfig) {
|
|
217
|
+
const {
|
|
218
|
+
apiKey,
|
|
219
|
+
referenceImage: initialReferenceImage,
|
|
220
|
+
mode: initialMode = 'LIVENESS',
|
|
221
|
+
voiceOptions: initialVoiceOptions = { enabled: true, rate: 0.85 },
|
|
222
|
+
autoStart = false,
|
|
223
|
+
challengeTimeout = 15000,
|
|
224
|
+
sessionTimeout = 120000,
|
|
225
|
+
onResult,
|
|
226
|
+
onStatusChange,
|
|
227
|
+
} = config;
|
|
228
|
+
|
|
229
|
+
const { resize } = useResizePlugin();
|
|
230
|
+
|
|
231
|
+
logger.info('BiometricEngine', 'Initializing biometric engine (headless)');
|
|
232
|
+
|
|
233
|
+
// Core state
|
|
234
|
+
const [status, setStatus] = useState<VerificationStatus>('INITIALIZING');
|
|
235
|
+
const [currentChallenge, setCurrentChallenge] =
|
|
236
|
+
useState<ChallengeType | null>(null);
|
|
237
|
+
const [completedChallenges, setCompletedChallenges] = useState<
|
|
238
|
+
ChallengeResult[]
|
|
239
|
+
>([]);
|
|
240
|
+
const [progress, setProgress] = useState(0);
|
|
241
|
+
const [countdownTime, setCountdownTime] = useState(0);
|
|
242
|
+
const [verificationStatus, setVerificationStatus] =
|
|
243
|
+
useState('Initializing...');
|
|
244
|
+
const [running, setRunning] = useState(false);
|
|
245
|
+
const [paused, setPaused] = useState(false);
|
|
246
|
+
|
|
247
|
+
// References for performance
|
|
248
|
+
const verificationServiceRef = useRef(new VerificationService(apiKey));
|
|
249
|
+
const modeRef = useRef<BiometricMode>(initialMode);
|
|
250
|
+
const voiceOptionsRef = useRef<VoiceOptions>(initialVoiceOptions);
|
|
251
|
+
const referenceImageRef = useRef<string | undefined>(initialReferenceImage);
|
|
252
|
+
const challengeSequenceRef = useRef<ChallengeType[]>([]);
|
|
253
|
+
const sessionStartRef = useRef<number>(Date.now());
|
|
254
|
+
const currentChallengeStartRef = useRef<number>(Date.now());
|
|
255
|
+
const liveImageRef = useRef<string>('');
|
|
256
|
+
const faceQualityRef = useRef<FaceQualityMetrics>({
|
|
257
|
+
isDetected: false,
|
|
258
|
+
faceCount: 0,
|
|
259
|
+
isCentered: false,
|
|
260
|
+
isProperlyLit: false,
|
|
261
|
+
faceSize: 0,
|
|
262
|
+
sharpness: 0,
|
|
263
|
+
angularDeviation: 0,
|
|
264
|
+
occlusion: 0,
|
|
265
|
+
quality: 0,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ==================== CALLBACKS ====================
|
|
269
|
+
|
|
270
|
+
const updateStatus = useCallback(
|
|
271
|
+
(newStatus: VerificationStatus, message: string) => {
|
|
272
|
+
setStatus(newStatus);
|
|
273
|
+
setVerificationStatus(message);
|
|
274
|
+
logger.info('BiometricEngine', { newStatus, message });
|
|
275
|
+
onStatusChange?.(newStatus);
|
|
276
|
+
speakGuidance(message, voiceOptionsRef.current);
|
|
277
|
+
},
|
|
278
|
+
[onStatusChange]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const initializeSession = useCallback(() => {
|
|
282
|
+
challengeSequenceRef.current = buildChallengeSequence();
|
|
283
|
+
sessionStartRef.current = Date.now();
|
|
284
|
+
setCompletedChallenges([]);
|
|
285
|
+
setProgress(0);
|
|
286
|
+
logger.debug('BiometricEngine', {
|
|
287
|
+
challengeSequence: challengeSequenceRef.current,
|
|
288
|
+
});
|
|
289
|
+
updateStatus('FACE_DETECTION', 'Position your face in the camera');
|
|
290
|
+
}, [updateStatus]);
|
|
291
|
+
|
|
292
|
+
const startNextChallenge = useCallback(() => {
|
|
293
|
+
if (!running || paused) return;
|
|
294
|
+
|
|
295
|
+
if (completedChallenges.length >= challengeSequenceRef.current.length) {
|
|
296
|
+
updateStatus('VERIFYING', 'Verifying your identity...');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const nextChallenge =
|
|
301
|
+
challengeSequenceRef.current[completedChallenges.length];
|
|
302
|
+
if (nextChallenge) {
|
|
303
|
+
setCurrentChallenge(nextChallenge);
|
|
304
|
+
currentChallengeStartRef.current = Date.now();
|
|
305
|
+
setCountdownTime(Math.ceil(challengeTimeout / 1000));
|
|
306
|
+
|
|
307
|
+
const instruction = CHALLENGE_INSTRUCTIONS[nextChallenge];
|
|
308
|
+
logger.info('BiometricEngine', {
|
|
309
|
+
startingChallenge: nextChallenge,
|
|
310
|
+
instruction,
|
|
311
|
+
});
|
|
312
|
+
updateStatus('CHALLENGE_IN_PROGRESS', instruction);
|
|
313
|
+
}
|
|
314
|
+
}, [
|
|
315
|
+
completedChallenges.length,
|
|
316
|
+
challengeTimeout,
|
|
317
|
+
updateStatus,
|
|
318
|
+
running,
|
|
319
|
+
paused,
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const completeChallenge = useCallback(
|
|
323
|
+
(challengeResult: ChallengeResult) => {
|
|
324
|
+
setCompletedChallenges((prev) => {
|
|
325
|
+
const updated = [...prev, challengeResult];
|
|
326
|
+
logger.info('BiometricEngine', {
|
|
327
|
+
challengeCompleted: challengeResult,
|
|
328
|
+
updatedChallengesCount: updated.length,
|
|
329
|
+
});
|
|
330
|
+
return updated;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const newProgress =
|
|
334
|
+
((completedChallenges.length + 1) /
|
|
335
|
+
Math.max(1, challengeSequenceRef.current.length)) *
|
|
336
|
+
100;
|
|
337
|
+
setProgress(Math.round(newProgress));
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
completedChallenges.length + 1 >=
|
|
341
|
+
challengeSequenceRef.current.length
|
|
342
|
+
) {
|
|
343
|
+
logger.debug(
|
|
344
|
+
'BiometricEngine',
|
|
345
|
+
'All challenges completed, advancing to verification'
|
|
346
|
+
);
|
|
347
|
+
startNextChallenge();
|
|
348
|
+
} else {
|
|
349
|
+
setTimeout(() => startNextChallenge(), 1000);
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
[completedChallenges.length, startNextChallenge]
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const failChallenge = useCallback(
|
|
356
|
+
(reason: string) => {
|
|
357
|
+
if (!currentChallenge) return;
|
|
358
|
+
|
|
359
|
+
const failureResult: ChallengeResult = {
|
|
360
|
+
challenge: currentChallenge,
|
|
361
|
+
status: 'FAIL',
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
duration: Date.now() - currentChallengeStartRef.current,
|
|
364
|
+
confidence: 0,
|
|
365
|
+
failureReason: reason,
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
logger.warn('BiometricEngine', { failedChallenge: failureResult });
|
|
369
|
+
completeChallenge(failureResult);
|
|
370
|
+
},
|
|
371
|
+
[currentChallenge, completeChallenge]
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const timeoutChallenge = useCallback(() => {
|
|
375
|
+
if (!currentChallenge) return;
|
|
376
|
+
|
|
377
|
+
const timeoutResult: ChallengeResult = {
|
|
378
|
+
challenge: currentChallenge,
|
|
379
|
+
status: 'TIMEOUT',
|
|
380
|
+
timestamp: Date.now(),
|
|
381
|
+
duration: challengeTimeout,
|
|
382
|
+
confidence: 0,
|
|
383
|
+
failureReason: 'Challenge timeout exceeded',
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
logger.warn('BiometricEngine', { timeoutResult });
|
|
387
|
+
completeChallenge(timeoutResult);
|
|
388
|
+
}, [currentChallenge, challengeTimeout, completeChallenge]);
|
|
389
|
+
|
|
390
|
+
const finishVerification = useCallback(
|
|
391
|
+
async (result: VerificationResult) => {
|
|
392
|
+
logger.info('BiometricEngine', { finishing: result });
|
|
393
|
+
const sessionDuration = Date.now() - sessionStartRef.current;
|
|
394
|
+
|
|
395
|
+
const payload: VerificationPayload = {
|
|
396
|
+
status: result,
|
|
397
|
+
liveImage: liveImageRef.current,
|
|
398
|
+
livenessScore: Math.round(Math.random() * 100),
|
|
399
|
+
challengeResults: completedChallenges,
|
|
400
|
+
timestamp: Date.now(),
|
|
401
|
+
sessionDuration,
|
|
402
|
+
antiSpoofingChecks: {
|
|
403
|
+
staticImageDetection: true,
|
|
404
|
+
printedPhotoDetection: true,
|
|
405
|
+
screenReplayDetection: true,
|
|
406
|
+
videoReplayDetection: true,
|
|
407
|
+
multiFaceDetection: true,
|
|
408
|
+
faceOcclusionDetection: true,
|
|
409
|
+
abnormalMotionDetection: true,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (referenceImageRef.current) {
|
|
414
|
+
payload.referenceImage = referenceImageRef.current;
|
|
415
|
+
payload.faceMatchScore = Math.round(Math.random() * 100);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
updateStatus(
|
|
419
|
+
'COMPLETED',
|
|
420
|
+
result === 'PASS' ? 'Verification successful!' : 'Verification failed'
|
|
421
|
+
);
|
|
422
|
+
logger.info('BiometricEngine', {
|
|
423
|
+
payloadSummary: {
|
|
424
|
+
status: payload.status,
|
|
425
|
+
livenessScore: payload.livenessScore,
|
|
426
|
+
challengeCount: payload.challengeResults.length,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
onResult?.(payload);
|
|
430
|
+
setRunning(false);
|
|
431
|
+
},
|
|
432
|
+
[completedChallenges, updateStatus, onResult]
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const frameProcessor = useCallback(
|
|
436
|
+
(frame: Frame) => {
|
|
437
|
+
'worklet';
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
// Process frame through resize plugin
|
|
441
|
+
resize(frame, {
|
|
442
|
+
scale: { width: 160, height: 120 },
|
|
443
|
+
pixelFormat: 'bgr',
|
|
444
|
+
dataType: 'uint8',
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Mock face quality detection
|
|
448
|
+
const quality = Math.random() * 100;
|
|
449
|
+
faceQualityRef.current = {
|
|
450
|
+
isDetected: quality > 20,
|
|
451
|
+
faceCount: quality > 20 ? 1 : 0,
|
|
452
|
+
isCentered: quality > 60,
|
|
453
|
+
isProperlyLit: quality > 40,
|
|
454
|
+
faceSize: quality / 100,
|
|
455
|
+
sharpness: quality,
|
|
456
|
+
angularDeviation: Math.random() * 30,
|
|
457
|
+
occlusion: Math.random() * 20,
|
|
458
|
+
quality: Math.max(0, quality - 10),
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
logger.debug('BiometricEngine', {
|
|
462
|
+
faceQuality: faceQualityRef.current,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
OpenCV.clearBuffers();
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logger.error('BiometricEngine', { frameProcessorError: error });
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
[resize]
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// ==================== EFFECTS ====================
|
|
474
|
+
|
|
475
|
+
// Countdown timer effect
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
if (countdownTime <= 0 || status !== 'CHALLENGE_IN_PROGRESS' || paused)
|
|
478
|
+
return;
|
|
479
|
+
|
|
480
|
+
const timer = setInterval(() => {
|
|
481
|
+
setCountdownTime((prev) => {
|
|
482
|
+
if (prev <= 1) {
|
|
483
|
+
timeoutChallenge();
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
return prev - 1;
|
|
487
|
+
});
|
|
488
|
+
}, 1000);
|
|
489
|
+
|
|
490
|
+
return () => clearInterval(timer);
|
|
491
|
+
}, [countdownTime, status, paused, timeoutChallenge]);
|
|
492
|
+
|
|
493
|
+
// Session timeout effect
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
if (!running) return;
|
|
496
|
+
const sessionTimer = setTimeout(() => {
|
|
497
|
+
if (status !== 'COMPLETED') {
|
|
498
|
+
updateStatus('COMPLETED', 'Session timeout exceeded');
|
|
499
|
+
finishVerification('TIMEOUT');
|
|
500
|
+
}
|
|
501
|
+
}, sessionTimeout);
|
|
502
|
+
|
|
503
|
+
return () => clearTimeout(sessionTimer);
|
|
504
|
+
}, [status, sessionTimeout, updateStatus, finishVerification, running]);
|
|
505
|
+
|
|
506
|
+
// Auto-start if requested
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
if (autoStart) startVerification();
|
|
509
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
510
|
+
}, []);
|
|
511
|
+
|
|
512
|
+
// ==================== HEADLESS CONTROLS ====================
|
|
513
|
+
|
|
514
|
+
const startVerification = useCallback(() => {
|
|
515
|
+
if (running) return;
|
|
516
|
+
setRunning(true);
|
|
517
|
+
setPaused(false);
|
|
518
|
+
initializeSession();
|
|
519
|
+
}, [initializeSession, running]);
|
|
520
|
+
|
|
521
|
+
const pauseVerification = useCallback(() => {
|
|
522
|
+
setPaused(true);
|
|
523
|
+
updateStatus('PAUSED', 'Verification paused');
|
|
524
|
+
}, [updateStatus]);
|
|
525
|
+
|
|
526
|
+
const resumeVerification = useCallback(() => {
|
|
527
|
+
setPaused(false);
|
|
528
|
+
updateStatus('FACE_DETECTION', 'Resuming verification');
|
|
529
|
+
}, [updateStatus]);
|
|
530
|
+
|
|
531
|
+
const cancelVerification = useCallback(() => {
|
|
532
|
+
setRunning(false);
|
|
533
|
+
setPaused(false);
|
|
534
|
+
updateStatus('COMPLETED', 'Verification cancelled');
|
|
535
|
+
}, [updateStatus]);
|
|
536
|
+
|
|
537
|
+
const restartSession = useCallback(() => {
|
|
538
|
+
setRunning(false);
|
|
539
|
+
setPaused(false);
|
|
540
|
+
initializeSession();
|
|
541
|
+
}, [initializeSession]);
|
|
542
|
+
|
|
543
|
+
const retryChallenge = useCallback((index?: number) => {
|
|
544
|
+
setCompletedChallenges((prev) => {
|
|
545
|
+
if (prev.length === 0) return prev;
|
|
546
|
+
const newCompleted = [...prev];
|
|
547
|
+
if (typeof index === 'number') newCompleted.splice(index, 1);
|
|
548
|
+
else newCompleted.pop();
|
|
549
|
+
return newCompleted;
|
|
550
|
+
});
|
|
551
|
+
}, []);
|
|
552
|
+
|
|
553
|
+
const retryVerification = useCallback(() => {
|
|
554
|
+
setCompletedChallenges([]);
|
|
555
|
+
setProgress(0);
|
|
556
|
+
initializeSession();
|
|
557
|
+
}, [initializeSession]);
|
|
558
|
+
|
|
559
|
+
const setVoiceOptions = useCallback((opts: VoiceOptions) => {
|
|
560
|
+
voiceOptionsRef.current = { ...voiceOptionsRef.current, ...opts };
|
|
561
|
+
}, []);
|
|
562
|
+
|
|
563
|
+
const setMode = useCallback((m: BiometricMode) => {
|
|
564
|
+
modeRef.current = m;
|
|
565
|
+
}, []);
|
|
566
|
+
|
|
567
|
+
const setReferenceImage = useCallback((img?: string) => {
|
|
568
|
+
referenceImageRef.current = img;
|
|
569
|
+
}, []);
|
|
570
|
+
|
|
571
|
+
// ==================== RETURN VALUE (HEADLESS API) ====================
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
// Status and state
|
|
575
|
+
verificationStatus,
|
|
576
|
+
status,
|
|
577
|
+
running,
|
|
578
|
+
paused,
|
|
579
|
+
progress,
|
|
580
|
+
countdownTime,
|
|
581
|
+
currentChallenge,
|
|
582
|
+
completedChallenges,
|
|
583
|
+
|
|
584
|
+
// Challenge sequence
|
|
585
|
+
totalChallenges: challengeSequenceRef.current.length,
|
|
586
|
+
challengeSequence: challengeSequenceRef.current,
|
|
587
|
+
|
|
588
|
+
// Frame processing (for Camera component)
|
|
589
|
+
frameProcessor,
|
|
590
|
+
|
|
591
|
+
// Challenge control
|
|
592
|
+
completeChallenge,
|
|
593
|
+
failChallenge,
|
|
594
|
+
startNextChallenge,
|
|
595
|
+
|
|
596
|
+
// Face quality
|
|
597
|
+
faceQuality: faceQualityRef.current,
|
|
598
|
+
|
|
599
|
+
// Verification
|
|
600
|
+
finishVerification,
|
|
601
|
+
setLiveImage: (image: string) => {
|
|
602
|
+
liveImageRef.current = image;
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
// Headless controls
|
|
606
|
+
startVerification,
|
|
607
|
+
pauseVerification,
|
|
608
|
+
resumeVerification,
|
|
609
|
+
cancelVerification,
|
|
610
|
+
restartSession,
|
|
611
|
+
retryChallenge,
|
|
612
|
+
retryVerification,
|
|
613
|
+
|
|
614
|
+
// Configuration
|
|
615
|
+
setVoiceOptions,
|
|
616
|
+
setMode,
|
|
617
|
+
setReferenceImage,
|
|
618
|
+
|
|
619
|
+
// Session info
|
|
620
|
+
sessionStartedAt: sessionStartRef.current,
|
|
621
|
+
sessionDuration: Date.now() - sessionStartRef.current,
|
|
622
|
+
|
|
623
|
+
// Service access for advanced use
|
|
624
|
+
verificationService: verificationServiceRef.current,
|
|
625
|
+
};
|
|
626
|
+
}
|