@hexar/biometric-identity-sdk-react-native 1.0.0

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.
Files changed (39) hide show
  1. package/README.md +68 -0
  2. package/dist/components/BiometricIdentityFlow.d.ts +17 -0
  3. package/dist/components/BiometricIdentityFlow.d.ts.map +1 -0
  4. package/dist/components/BiometricIdentityFlow.js +366 -0
  5. package/dist/components/CameraCapture.d.ts +15 -0
  6. package/dist/components/CameraCapture.d.ts.map +1 -0
  7. package/dist/components/CameraCapture.js +238 -0
  8. package/dist/components/ErrorScreen.d.ts +15 -0
  9. package/dist/components/ErrorScreen.d.ts.map +1 -0
  10. package/dist/components/ErrorScreen.js +142 -0
  11. package/dist/components/InstructionsScreen.d.ts +14 -0
  12. package/dist/components/InstructionsScreen.d.ts.map +1 -0
  13. package/dist/components/InstructionsScreen.js +181 -0
  14. package/dist/components/ResultScreen.d.ts +15 -0
  15. package/dist/components/ResultScreen.d.ts.map +1 -0
  16. package/dist/components/ResultScreen.js +182 -0
  17. package/dist/components/ValidationProgress.d.ts +14 -0
  18. package/dist/components/ValidationProgress.d.ts.map +1 -0
  19. package/dist/components/ValidationProgress.js +143 -0
  20. package/dist/components/VideoRecorder.d.ts +43 -0
  21. package/dist/components/VideoRecorder.d.ts.map +1 -0
  22. package/dist/components/VideoRecorder.js +631 -0
  23. package/dist/hooks/useBiometricSDK.d.ts +25 -0
  24. package/dist/hooks/useBiometricSDK.d.ts.map +1 -0
  25. package/dist/hooks/useBiometricSDK.js +173 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +47 -0
  29. package/package.json +27 -0
  30. package/src/components/BiometricIdentityFlow.tsx +557 -0
  31. package/src/components/CameraCapture.tsx +262 -0
  32. package/src/components/ErrorScreen.tsx +201 -0
  33. package/src/components/InstructionsScreen.tsx +269 -0
  34. package/src/components/ResultScreen.tsx +301 -0
  35. package/src/components/ValidationProgress.tsx +223 -0
  36. package/src/components/VideoRecorder.tsx +794 -0
  37. package/src/hooks/useBiometricSDK.ts +230 -0
  38. package/src/index.ts +24 -0
  39. package/tsconfig.json +20 -0
@@ -0,0 +1,557 @@
1
+ /**
2
+ * React Native Biometric Identity Flow Component
3
+ * Main UI component for identity verification with backend AI support
4
+ */
5
+
6
+ import React, { useState, useEffect, useCallback } from 'react';
7
+ import {
8
+ View,
9
+ Text,
10
+ StyleSheet,
11
+ TouchableOpacity,
12
+ ActivityIndicator,
13
+ SafeAreaView,
14
+ } from 'react-native';
15
+ import {
16
+ ValidationResult,
17
+ ThemeConfig,
18
+ BiometricError,
19
+ SDKStep,
20
+ getStrings,
21
+ setLanguage,
22
+ SupportedLanguage,
23
+ } from '@hexar/biometric-identity-sdk-core';
24
+ import type { ChallengeAction } from './VideoRecorder';
25
+ import { useBiometricSDK } from '../hooks/useBiometricSDK';
26
+ import { CameraCapture } from './CameraCapture';
27
+ import { VideoRecorder, VideoRecordingResult } from './VideoRecorder';
28
+ import { ValidationProgress } from './ValidationProgress';
29
+ import { ResultScreen } from './ResultScreen';
30
+ import { ErrorScreen } from './ErrorScreen';
31
+ import { InstructionsScreen } from './InstructionsScreen';
32
+
33
+ export interface BiometricIdentityFlowProps {
34
+ onValidationComplete: (result: ValidationResult) => void;
35
+ onError: (error: BiometricError) => void;
36
+ theme?: ThemeConfig;
37
+ language?: SupportedLanguage;
38
+ customTranslations?: Record<string, string>;
39
+ smartLivenessMode?: boolean;
40
+ }
41
+
42
+ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
43
+ onValidationComplete,
44
+ onError,
45
+ theme,
46
+ language = 'en',
47
+ customTranslations,
48
+ smartLivenessMode = true,
49
+ }) => {
50
+ const {
51
+ sdk,
52
+ state,
53
+ isInitialized,
54
+ isUsingBackend,
55
+ challenges,
56
+ uploadFrontID,
57
+ uploadBackID,
58
+ storeVideoRecording,
59
+ fetchChallenges,
60
+ validateIdentity,
61
+ reset,
62
+ } = useBiometricSDK();
63
+
64
+ const [showCamera, setShowCamera] = useState(false);
65
+ const [cameraMode, setCameraMode] = useState<'front' | 'back' | 'video'>('front');
66
+ const [showInstructions, setShowInstructions] = useState(true);
67
+ const [currentChallenges, setCurrentChallenges] = useState<ChallengeAction[]>([]);
68
+ const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
69
+
70
+ useEffect(() => {
71
+ setLanguage(language);
72
+ }, [language]);
73
+
74
+ const strings = getStrings();
75
+ const styles = createStyles(theme);
76
+
77
+ // Handle validation result
78
+ useEffect(() => {
79
+ if (state.validationResult) {
80
+ onValidationComplete(state.validationResult);
81
+ }
82
+ }, [state.validationResult, onValidationComplete]);
83
+
84
+ // Handle error
85
+ useEffect(() => {
86
+ if (state.error) {
87
+ onError(state.error);
88
+ }
89
+ }, [state.error, onError]);
90
+
91
+ /**
92
+ * Start capture process
93
+ */
94
+ const handleCaptureStart = useCallback(async (mode: 'front' | 'back' | 'video') => {
95
+ setCameraMode(mode);
96
+
97
+ // If video mode, fetch challenges first
98
+ if (mode === 'video' && smartLivenessMode && isUsingBackend) {
99
+ setIsLoadingChallenges(true);
100
+ try {
101
+ const challenges = await fetchChallenges('active');
102
+ setCurrentChallenges(challenges);
103
+ } catch (error) {
104
+ console.warn('Failed to fetch challenges, using defaults');
105
+ setCurrentChallenges(sdk.getDefaultChallenges());
106
+ }
107
+ setIsLoadingChallenges(false);
108
+ } else if (mode === 'video' && smartLivenessMode) {
109
+ // Use default challenges for offline mode
110
+ setCurrentChallenges(sdk.getDefaultChallenges());
111
+ }
112
+
113
+ setShowCamera(true);
114
+ }, [smartLivenessMode, isUsingBackend, fetchChallenges, sdk]);
115
+
116
+ /**
117
+ * Handle capture completion
118
+ */
119
+ const handleCaptureComplete = useCallback(async (data: any) => {
120
+ setShowCamera(false);
121
+
122
+ try {
123
+ if (cameraMode === 'front') {
124
+ await uploadFrontID(data);
125
+ } else if (cameraMode === 'back') {
126
+ await uploadBackID(data);
127
+ } else if (cameraMode === 'video') {
128
+ // Handle video recording result
129
+ const videoResult: VideoRecordingResult = data;
130
+
131
+ await storeVideoRecording({
132
+ frames: videoResult.frames,
133
+ duration: videoResult.duration,
134
+ instructionsFollowed: videoResult.instructionsFollowed,
135
+ qualityScore: videoResult.qualityScore,
136
+ challengesCompleted: videoResult.challengesCompleted,
137
+ sessionId: videoResult.sessionId,
138
+ });
139
+
140
+ // Automatically start validation after video
141
+ await validateIdentity();
142
+ }
143
+ } catch (error) {
144
+ console.error('Capture error:', error);
145
+ onError({
146
+ name: 'BiometricError',
147
+ message: error instanceof Error ? error.message : 'Unknown error during capture',
148
+ code: 'LIVENESS_CHECK_FAILED',
149
+ } as BiometricError);
150
+ }
151
+ }, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, onError]);
152
+
153
+ /**
154
+ * Handle retry
155
+ */
156
+ const handleRetry = useCallback(() => {
157
+ reset();
158
+ setCurrentChallenges([]);
159
+ setShowInstructions(true);
160
+ }, [reset]);
161
+
162
+ // Show loading while initializing
163
+ if (!isInitialized) {
164
+ return (
165
+ <SafeAreaView style={styles.container}>
166
+ <View style={styles.loadingFullScreen}>
167
+ <ActivityIndicator size="large" color={theme?.primaryColor || '#6366F1'} />
168
+ <Text style={styles.loadingText}>{strings.initialization.initializing}</Text>
169
+ </View>
170
+ </SafeAreaView>
171
+ );
172
+ }
173
+
174
+ // Show instructions on first load
175
+ if (showInstructions) {
176
+ return (
177
+ <InstructionsScreen
178
+ theme={theme}
179
+ language={language}
180
+ onStart={() => setShowInstructions(false)}
181
+ />
182
+ );
183
+ }
184
+
185
+ // Show camera/video recorder
186
+ if (showCamera) {
187
+ if (cameraMode === 'video') {
188
+ return (
189
+ <VideoRecorder
190
+ theme={theme}
191
+ challenges={currentChallenges}
192
+ smartMode={smartLivenessMode}
193
+ sessionId={sdk.getSessionId() || undefined}
194
+ onComplete={handleCaptureComplete}
195
+ onCancel={() => setShowCamera(false)}
196
+ onFetchChallenges={async () => {
197
+ const challenges = await fetchChallenges('active');
198
+ return challenges;
199
+ }}
200
+ />
201
+ );
202
+ }
203
+
204
+ return (
205
+ <CameraCapture
206
+ mode={cameraMode}
207
+ theme={theme}
208
+ onCapture={handleCaptureComplete}
209
+ onCancel={() => setShowCamera(false)}
210
+ />
211
+ );
212
+ }
213
+
214
+ // Show validation progress
215
+ if (state.currentStep === SDKStep.VALIDATING) {
216
+ return (
217
+ <ValidationProgress
218
+ progress={state.progress}
219
+ theme={theme}
220
+ language={language}
221
+ />
222
+ );
223
+ }
224
+
225
+ // Show result
226
+ if (state.currentStep === SDKStep.RESULT && state.validationResult) {
227
+ return (
228
+ <ResultScreen
229
+ result={state.validationResult}
230
+ theme={theme}
231
+ language={language}
232
+ onClose={() => onValidationComplete(state.validationResult!)}
233
+ />
234
+ );
235
+ }
236
+
237
+ // Show error
238
+ if (state.currentStep === SDKStep.ERROR && state.error) {
239
+ return (
240
+ <ErrorScreen
241
+ error={state.error}
242
+ theme={theme}
243
+ language={language}
244
+ onRetry={handleRetry}
245
+ onClose={() => onError(state.error!)}
246
+ />
247
+ );
248
+ }
249
+
250
+ return (
251
+ <SafeAreaView style={styles.container}>
252
+ <View style={styles.content}>
253
+ <Text style={styles.title}>{strings.instructions.title}</Text>
254
+ <Text style={styles.subtitle}>{strings.instructions.subtitle}</Text>
255
+
256
+ {isUsingBackend && (
257
+ <View style={styles.backendBadge}>
258
+ <View style={styles.backendDot} />
259
+ <Text style={styles.backendText}>{strings.badges.secureVerification}</Text>
260
+ </View>
261
+ )}
262
+
263
+ <View style={styles.progressContainer}>
264
+ <StepIndicator
265
+ step={1}
266
+ active={state.currentStep === SDKStep.CAPTURE_FRONT_ID}
267
+ completed={!!state.frontID}
268
+ label={strings.steps.frontId}
269
+ theme={theme}
270
+ />
271
+ <View style={styles.stepConnector} />
272
+ <StepIndicator
273
+ step={2}
274
+ active={state.currentStep === SDKStep.CAPTURE_BACK_ID}
275
+ completed={!!state.backID}
276
+ label={strings.steps.backId}
277
+ theme={theme}
278
+ />
279
+ <View style={styles.stepConnector} />
280
+ <StepIndicator
281
+ step={3}
282
+ active={state.currentStep === SDKStep.RECORD_LIVENESS}
283
+ completed={!!state.videoData}
284
+ label={strings.steps.faceVideo}
285
+ theme={theme}
286
+ />
287
+ </View>
288
+
289
+ <View style={styles.actionsContainer}>
290
+ {!state.frontID && (
291
+ <ActionButton
292
+ title={strings.capture.frontId.button}
293
+ subtitle={strings.capture.frontId.instruction}
294
+ onPress={() => handleCaptureStart('front')}
295
+ theme={theme}
296
+ disabled={state.isLoading}
297
+ />
298
+ )}
299
+
300
+ {state.frontID && !state.backID && (
301
+ <ActionButton
302
+ title={strings.capture.backId.button}
303
+ subtitle={strings.capture.backId.instruction}
304
+ onPress={() => handleCaptureStart('back')}
305
+ theme={theme}
306
+ disabled={state.isLoading}
307
+ />
308
+ )}
309
+
310
+ {state.frontID && state.backID && !state.videoData && (
311
+ <ActionButton
312
+ title={strings.liveness.title}
313
+ subtitle={strings.liveness.instructions.stayStill}
314
+ onPress={() => handleCaptureStart('video')}
315
+ theme={theme}
316
+ disabled={state.isLoading || isLoadingChallenges}
317
+ />
318
+ )}
319
+ </View>
320
+
321
+ {(state.isLoading || isLoadingChallenges) && (
322
+ <View style={styles.loadingContainer}>
323
+ <ActivityIndicator size="large" color={theme?.primaryColor || '#6366F1'} />
324
+ <Text style={styles.loadingText}>
325
+ {isLoadingChallenges ? strings.liveness.preparing : strings.common.loading}
326
+ </Text>
327
+ </View>
328
+ )}
329
+ </View>
330
+ </SafeAreaView>
331
+ );
332
+ };
333
+
334
+ // Step Indicator Component
335
+ interface StepIndicatorProps {
336
+ step: number;
337
+ active: boolean;
338
+ completed: boolean;
339
+ label: string;
340
+ theme?: ThemeConfig;
341
+ }
342
+
343
+ const StepIndicator: React.FC<StepIndicatorProps> = ({
344
+ step,
345
+ active,
346
+ completed,
347
+ label,
348
+ theme,
349
+ }) => {
350
+ const primaryColor = theme?.primaryColor || '#6366F1';
351
+ const successColor = theme?.successColor || '#10B981';
352
+
353
+ return (
354
+ <View style={stepStyles.container}>
355
+ <View
356
+ style={[
357
+ stepStyles.circle,
358
+ active && { backgroundColor: primaryColor, borderColor: primaryColor },
359
+ completed && { backgroundColor: successColor, borderColor: successColor },
360
+ ]}
361
+ >
362
+ <Text style={[stepStyles.stepNumber, (active || completed) && stepStyles.stepNumberActive]}>
363
+ {completed ? '✓' : step}
364
+ </Text>
365
+ </View>
366
+ <Text style={[stepStyles.label, active && { color: primaryColor }]}>{label}</Text>
367
+ </View>
368
+ );
369
+ };
370
+
371
+ const stepStyles = StyleSheet.create({
372
+ container: {
373
+ alignItems: 'center',
374
+ flex: 1,
375
+ },
376
+ circle: {
377
+ width: 44,
378
+ height: 44,
379
+ borderRadius: 22,
380
+ backgroundColor: '#F3F4F6',
381
+ borderWidth: 2,
382
+ borderColor: '#E5E7EB',
383
+ justifyContent: 'center',
384
+ alignItems: 'center',
385
+ },
386
+ stepNumber: {
387
+ fontSize: 16,
388
+ fontWeight: '600',
389
+ color: '#6B7280',
390
+ },
391
+ stepNumberActive: {
392
+ color: '#FFFFFF',
393
+ },
394
+ label: {
395
+ marginTop: 8,
396
+ fontSize: 12,
397
+ color: '#6B7280',
398
+ textAlign: 'center',
399
+ },
400
+ });
401
+
402
+ // Action Button Component
403
+ interface ActionButtonProps {
404
+ title: string;
405
+ subtitle?: string;
406
+ onPress: () => void;
407
+ theme?: ThemeConfig;
408
+ disabled?: boolean;
409
+ }
410
+
411
+ const ActionButton: React.FC<ActionButtonProps> = ({
412
+ title,
413
+ subtitle,
414
+ onPress,
415
+ theme,
416
+ disabled,
417
+ }) => {
418
+ const primaryColor = theme?.primaryColor || '#6366F1';
419
+
420
+ return (
421
+ <TouchableOpacity
422
+ style={[
423
+ buttonStyles.button,
424
+ { backgroundColor: primaryColor },
425
+ disabled && buttonStyles.buttonDisabled,
426
+ ]}
427
+ onPress={onPress}
428
+ disabled={disabled}
429
+ >
430
+ <Text style={buttonStyles.buttonText}>{title}</Text>
431
+ {subtitle && (
432
+ <Text style={buttonStyles.buttonSubtext}>{subtitle}</Text>
433
+ )}
434
+ </TouchableOpacity>
435
+ );
436
+ };
437
+
438
+ const buttonStyles = StyleSheet.create({
439
+ button: {
440
+ paddingVertical: 16,
441
+ paddingHorizontal: 32,
442
+ borderRadius: 12,
443
+ alignItems: 'center',
444
+ marginVertical: 8,
445
+ shadowColor: '#000',
446
+ shadowOffset: { width: 0, height: 2 },
447
+ shadowOpacity: 0.1,
448
+ shadowRadius: 4,
449
+ elevation: 3,
450
+ },
451
+ buttonDisabled: {
452
+ opacity: 0.5,
453
+ },
454
+ buttonText: {
455
+ color: '#FFFFFF',
456
+ fontSize: 18,
457
+ fontWeight: '700',
458
+ },
459
+ buttonSubtext: {
460
+ color: 'rgba(255, 255, 255, 0.8)',
461
+ fontSize: 13,
462
+ marginTop: 4,
463
+ },
464
+ });
465
+
466
+ // Create styles based on theme
467
+ const createStyles = (theme?: ThemeConfig) => {
468
+ const backgroundColor = theme?.backgroundColor || '#FFFFFF';
469
+ const textColor = theme?.textColor || '#000000';
470
+ const secondaryTextColor = theme?.secondaryTextColor || '#6B7280';
471
+ const primaryColor = theme?.primaryColor || '#6366F1';
472
+
473
+ return StyleSheet.create({
474
+ container: {
475
+ flex: 1,
476
+ backgroundColor,
477
+ },
478
+ content: {
479
+ flex: 1,
480
+ padding: 24,
481
+ },
482
+ title: {
483
+ fontSize: 28,
484
+ fontWeight: 'bold',
485
+ color: textColor,
486
+ marginBottom: 8,
487
+ textAlign: 'center',
488
+ },
489
+ subtitle: {
490
+ fontSize: 16,
491
+ color: secondaryTextColor,
492
+ marginBottom: 24,
493
+ textAlign: 'center',
494
+ },
495
+ backendBadge: {
496
+ flexDirection: 'row',
497
+ alignItems: 'center',
498
+ justifyContent: 'center',
499
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
500
+ paddingHorizontal: 12,
501
+ paddingVertical: 6,
502
+ borderRadius: 16,
503
+ alignSelf: 'center',
504
+ marginBottom: 24,
505
+ },
506
+ backendDot: {
507
+ width: 8,
508
+ height: 8,
509
+ borderRadius: 4,
510
+ backgroundColor: '#10B981',
511
+ marginRight: 8,
512
+ },
513
+ backendText: {
514
+ color: '#10B981',
515
+ fontSize: 12,
516
+ fontWeight: '600',
517
+ },
518
+ progressContainer: {
519
+ flexDirection: 'row',
520
+ justifyContent: 'space-around',
521
+ alignItems: 'center',
522
+ marginBottom: 48,
523
+ paddingHorizontal: 16,
524
+ },
525
+ stepConnector: {
526
+ flex: 0.5,
527
+ height: 2,
528
+ backgroundColor: '#E5E7EB',
529
+ marginHorizontal: -10,
530
+ },
531
+ actionsContainer: {
532
+ marginTop: 24,
533
+ },
534
+ loadingContainer: {
535
+ marginTop: 32,
536
+ alignItems: 'center',
537
+ },
538
+ loadingFullScreen: {
539
+ flex: 1,
540
+ justifyContent: 'center',
541
+ alignItems: 'center',
542
+ },
543
+ loadingText: {
544
+ marginTop: 12,
545
+ fontSize: 14,
546
+ color: secondaryTextColor,
547
+ },
548
+ loadingSubtext: {
549
+ marginTop: 4,
550
+ fontSize: 12,
551
+ color: secondaryTextColor,
552
+ opacity: 0.7,
553
+ },
554
+ });
555
+ };
556
+
557
+ export default BiometricIdentityFlow;