@kernaq/verify-react-native 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1200 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var reactNative = require('react-native');
5
+ var reactNativePermissions = require('react-native-permissions');
6
+ var reactNativeVisionCamera = require('react-native-vision-camera');
7
+
8
+ const DEFAULT_LOCALE = {
9
+ intro_title: 'Verify your identity',
10
+ intro_body: "This takes about 60 seconds. You'll need your ID document and a clear view of your face.",
11
+ intro_cta: 'Get started',
12
+ doc_title: 'Scan your document',
13
+ doc_instruction: 'Hold your ID flat inside the frame. Make sure it\'s well-lit and all four corners are visible.',
14
+ doc_capture_btn: 'Capture',
15
+ doc_retake_btn: 'Retake',
16
+ doc_next_btn: 'Looks good',
17
+ selfie_title: 'Take a selfie',
18
+ selfie_instruction: 'Look straight at the camera. Keep your face inside the oval.',
19
+ selfie_capture_btn: 'Take selfie',
20
+ selfie_retake_btn: 'Retake',
21
+ selfie_next_btn: 'Looks good',
22
+ liveness_title: 'Liveness check',
23
+ liveness_instruction: 'Press start, then slowly turn your head left and right.',
24
+ liveness_start_btn: 'Start recording',
25
+ liveness_recording: 'Recording…',
26
+ processing_title: 'Verifying your identity',
27
+ processing_subtitle: 'This usually takes a few seconds.',
28
+ result_pass_title: 'Verification passed',
29
+ result_pass_body: 'Your identity has been verified successfully.',
30
+ result_fail_title: 'Verification failed',
31
+ result_fail_body: 'We were unable to verify your identity. Please try again.',
32
+ result_review_title: 'Under review',
33
+ result_review_body: 'Your submission is being reviewed. This usually takes a few minutes.',
34
+ result_retry_btn: 'Try again',
35
+ error_camera_denied: 'Camera access denied. Please allow camera permissions in settings.',
36
+ error_quality: 'Image quality too low. Try better lighting or hold the camera steady.',
37
+ error_network: 'Network error. Please check your connection and try again.'
38
+ };
39
+
40
+ function resolveTheme(theme) {
41
+ const dark = theme?.mode === 'dark';
42
+ return {
43
+ accent: theme?.accentColor ?? '#111827',
44
+ bg: theme?.backgroundColor ?? (dark ? '#1a1a2e' : '#ffffff'),
45
+ text: theme?.textColor ?? (dark ? '#f9fafb' : '#111827'),
46
+ subtext: theme?.subtextColor ?? (dark ? '#9ca3af' : '#6b7280'),
47
+ border: theme?.borderColor ?? (dark ? '#374151' : '#e5e7eb'),
48
+ card: theme?.cardColor ?? (dark ? '#111827' : '#f9fafb'),
49
+ radius: theme?.borderRadius ?? 12
50
+ };
51
+ }
52
+
53
+ function usePermissions() {
54
+ const [status, setStatus] = React.useState('unknown');
55
+ const cameraPermission = reactNative.Platform.OS === 'ios' ? reactNativePermissions.PERMISSIONS.IOS.CAMERA : reactNativePermissions.PERMISSIONS.ANDROID.CAMERA;
56
+ const micPermission = reactNative.Platform.OS === 'ios' ? reactNativePermissions.PERMISSIONS.IOS.MICROPHONE : reactNativePermissions.PERMISSIONS.ANDROID.RECORD_AUDIO;
57
+ const requestPermissions = React.useCallback(async ()=>{
58
+ try {
59
+ const camResult = await reactNativePermissions.check(cameraPermission);
60
+ if (camResult === reactNativePermissions.RESULTS.GRANTED) {
61
+ // Also request mic for liveness video
62
+ await reactNativePermissions.request(micPermission);
63
+ setStatus('granted');
64
+ return true;
65
+ }
66
+ if (camResult === reactNativePermissions.RESULTS.BLOCKED) {
67
+ setStatus('blocked');
68
+ reactNative.Alert.alert('Camera Permission Required', 'Camera access is blocked. Please enable it in your device settings.', [
69
+ {
70
+ text: 'Cancel',
71
+ style: 'cancel'
72
+ },
73
+ {
74
+ text: 'Open Settings',
75
+ onPress: ()=>reactNative.Linking.openSettings()
76
+ }
77
+ ]);
78
+ return false;
79
+ }
80
+ // Request camera
81
+ const result = await reactNativePermissions.request(cameraPermission);
82
+ if (result === reactNativePermissions.RESULTS.GRANTED) {
83
+ await reactNativePermissions.request(micPermission);
84
+ setStatus('granted');
85
+ return true;
86
+ }
87
+ setStatus('denied');
88
+ return false;
89
+ } catch {
90
+ setStatus('denied');
91
+ return false;
92
+ }
93
+ }, [
94
+ cameraPermission,
95
+ micPermission
96
+ ]);
97
+ return {
98
+ status,
99
+ requestPermissions
100
+ };
101
+ }
102
+
103
+ function IntroStep({ theme, locale, onStart }) {
104
+ const s = styles$6(theme);
105
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
106
+ style: s.container
107
+ }, /*#__PURE__*/ React.createElement(reactNative.View, {
108
+ style: s.steps
109
+ }, [
110
+ {
111
+ icon: '🪪',
112
+ label: 'Document'
113
+ },
114
+ {
115
+ icon: '🤳',
116
+ label: 'Selfie'
117
+ },
118
+ {
119
+ icon: '👁️',
120
+ label: 'Liveness'
121
+ }
122
+ ].map((item)=>/*#__PURE__*/ React.createElement(reactNative.View, {
123
+ key: item.label,
124
+ style: s.step
125
+ }, /*#__PURE__*/ React.createElement(reactNative.View, {
126
+ style: s.stepIcon
127
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
128
+ style: s.stepEmoji
129
+ }, item.icon)), /*#__PURE__*/ React.createElement(reactNative.Text, {
130
+ style: s.stepLabel
131
+ }, item.label)))), /*#__PURE__*/ React.createElement(reactNative.Text, {
132
+ style: s.title
133
+ }, locale.intro_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
134
+ style: s.body
135
+ }, locale.intro_body), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
136
+ style: s.btn,
137
+ onPress: onStart,
138
+ activeOpacity: 0.85
139
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
140
+ style: s.btnText
141
+ }, locale.intro_cta)));
142
+ }
143
+ const styles$6 = (t)=>reactNative.StyleSheet.create({
144
+ container: {
145
+ padding: 24
146
+ },
147
+ steps: {
148
+ flexDirection: 'row',
149
+ justifyContent: 'center',
150
+ gap: 20,
151
+ marginBottom: 28
152
+ },
153
+ step: {
154
+ alignItems: 'center',
155
+ gap: 8
156
+ },
157
+ stepIcon: {
158
+ width: 56,
159
+ height: 56,
160
+ borderRadius: 14,
161
+ backgroundColor: t.card,
162
+ borderWidth: 1,
163
+ borderColor: t.border,
164
+ alignItems: 'center',
165
+ justifyContent: 'center'
166
+ },
167
+ stepEmoji: {
168
+ fontSize: 24
169
+ },
170
+ stepLabel: {
171
+ fontSize: 12,
172
+ color: t.subtext
173
+ },
174
+ title: {
175
+ fontSize: 22,
176
+ fontWeight: '700',
177
+ color: t.text,
178
+ marginBottom: 10
179
+ },
180
+ body: {
181
+ fontSize: 14,
182
+ color: t.subtext,
183
+ lineHeight: 21,
184
+ marginBottom: 28
185
+ },
186
+ btn: {
187
+ backgroundColor: t.accent,
188
+ borderRadius: t.radius,
189
+ paddingVertical: 14,
190
+ alignItems: 'center'
191
+ },
192
+ btnText: {
193
+ color: '#fff',
194
+ fontSize: 15,
195
+ fontWeight: '600'
196
+ }
197
+ });
198
+
199
+ function useCamera(facing = 'back') {
200
+ const cameraRef = React.useRef(null);
201
+ const device = reactNativeVisionCamera.useCameraDevice(facing);
202
+ const { hasPermission } = reactNativeVisionCamera.useCameraPermission();
203
+ const capturePhoto = React.useCallback(async ()=>{
204
+ if (!cameraRef.current) return null;
205
+ try {
206
+ const photo = await cameraRef.current.takePhoto({
207
+ flash: 'off',
208
+ enableShutterSound: false
209
+ });
210
+ return {
211
+ uri: `file://${photo.path}`,
212
+ width: photo.width,
213
+ height: photo.height
214
+ };
215
+ } catch {
216
+ return null;
217
+ }
218
+ }, []);
219
+ const startRecording = React.useCallback((onFinished, onError)=>{
220
+ if (!cameraRef.current) return;
221
+ cameraRef.current.startRecording({
222
+ flash: 'off',
223
+ onRecordingFinished: (video)=>{
224
+ onFinished({
225
+ uri: `file://${video.path}`,
226
+ duration: video.duration
227
+ });
228
+ },
229
+ onRecordingError: onError
230
+ });
231
+ }, []);
232
+ const stopRecording = React.useCallback(async ()=>{
233
+ if (!cameraRef.current) return;
234
+ await cameraRef.current.stopRecording();
235
+ }, []);
236
+ return {
237
+ cameraRef,
238
+ device,
239
+ hasPermission,
240
+ capturePhoto,
241
+ startRecording,
242
+ stopRecording
243
+ };
244
+ }
245
+
246
+ function DocumentStep({ theme, locale, onCapture }) {
247
+ const { cameraRef, device, capturePhoto } = useCamera('back');
248
+ const [preview, setPreview] = React.useState(null);
249
+ const [capturing, setCapturing] = React.useState(false);
250
+ const s = styles$5(theme);
251
+ const handleCapture = async ()=>{
252
+ setCapturing(true);
253
+ const result = await capturePhoto();
254
+ setCapturing(false);
255
+ if (result) setPreview(result.uri);
256
+ };
257
+ const handleConfirm = ()=>{
258
+ if (preview) onCapture(preview);
259
+ };
260
+ if (!device) {
261
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
262
+ style: s.container
263
+ }, /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
264
+ color: theme.accent
265
+ }));
266
+ }
267
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
268
+ style: s.container
269
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
270
+ style: s.title
271
+ }, locale.doc_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
272
+ style: s.subtitle
273
+ }, locale.doc_instruction), preview ? /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(reactNative.Image, {
274
+ source: {
275
+ uri: preview
276
+ },
277
+ style: s.preview,
278
+ resizeMode: "cover"
279
+ }), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
280
+ style: s.btnPrimary,
281
+ onPress: handleConfirm,
282
+ activeOpacity: 0.85
283
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
284
+ style: s.btnText
285
+ }, locale.doc_next_btn)), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
286
+ style: s.btnSecondary,
287
+ onPress: ()=>setPreview(null),
288
+ activeOpacity: 0.75
289
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
290
+ style: s.btnSecondaryText
291
+ }, locale.doc_retake_btn))) : /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(reactNative.View, {
292
+ style: s.viewport
293
+ }, /*#__PURE__*/ React.createElement(reactNativeVisionCamera.Camera, {
294
+ ref: cameraRef,
295
+ style: reactNative.StyleSheet.absoluteFill,
296
+ device: device,
297
+ isActive: true,
298
+ photo: true
299
+ }), /*#__PURE__*/ React.createElement(reactNative.View, {
300
+ style: s.frameGuide,
301
+ pointerEvents: "none"
302
+ })), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
303
+ style: [
304
+ s.btnPrimary,
305
+ capturing && s.btnDisabled
306
+ ],
307
+ onPress: handleCapture,
308
+ disabled: capturing,
309
+ activeOpacity: 0.85
310
+ }, capturing ? /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
311
+ color: "#fff"
312
+ }) : /*#__PURE__*/ React.createElement(reactNative.Text, {
313
+ style: s.btnText
314
+ }, locale.doc_capture_btn))));
315
+ }
316
+ const styles$5 = (t)=>reactNative.StyleSheet.create({
317
+ container: {
318
+ padding: 24
319
+ },
320
+ title: {
321
+ fontSize: 20,
322
+ fontWeight: '700',
323
+ color: t.text,
324
+ marginBottom: 8
325
+ },
326
+ subtitle: {
327
+ fontSize: 14,
328
+ color: t.subtext,
329
+ lineHeight: 20,
330
+ marginBottom: 16
331
+ },
332
+ viewport: {
333
+ width: '100%',
334
+ aspectRatio: 4 / 3,
335
+ borderRadius: t.radius,
336
+ overflow: 'hidden',
337
+ backgroundColor: '#000',
338
+ marginBottom: 16,
339
+ position: 'relative'
340
+ },
341
+ frameGuide: {
342
+ position: 'absolute',
343
+ top: '8%',
344
+ left: '5%',
345
+ right: '5%',
346
+ bottom: '8%',
347
+ borderWidth: 2,
348
+ borderColor: 'rgba(255,255,255,0.7)',
349
+ borderRadius: 8
350
+ },
351
+ preview: {
352
+ width: '100%',
353
+ aspectRatio: 4 / 3,
354
+ borderRadius: t.radius,
355
+ marginBottom: 16,
356
+ borderWidth: 1,
357
+ borderColor: t.border
358
+ },
359
+ btnPrimary: {
360
+ backgroundColor: t.accent,
361
+ borderRadius: t.radius,
362
+ paddingVertical: 14,
363
+ alignItems: 'center',
364
+ marginBottom: 10
365
+ },
366
+ btnSecondary: {
367
+ backgroundColor: t.card,
368
+ borderRadius: t.radius,
369
+ paddingVertical: 14,
370
+ alignItems: 'center',
371
+ borderWidth: 1,
372
+ borderColor: t.border
373
+ },
374
+ btnDisabled: {
375
+ opacity: 0.6
376
+ },
377
+ btnText: {
378
+ color: '#fff',
379
+ fontSize: 15,
380
+ fontWeight: '600'
381
+ },
382
+ btnSecondaryText: {
383
+ color: t.text,
384
+ fontSize: 15,
385
+ fontWeight: '600'
386
+ }
387
+ });
388
+
389
+ function SelfieStep({ theme, locale, onCapture, onBack }) {
390
+ const { cameraRef, device, capturePhoto } = useCamera('front');
391
+ const [preview, setPreview] = React.useState(null);
392
+ const [capturing, setCapturing] = React.useState(false);
393
+ const s = styles$4(theme);
394
+ const handleCapture = async ()=>{
395
+ setCapturing(true);
396
+ const result = await capturePhoto();
397
+ setCapturing(false);
398
+ if (result) setPreview(result.uri);
399
+ };
400
+ if (!device) return /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
401
+ color: theme.accent,
402
+ style: {
403
+ margin: 40
404
+ }
405
+ });
406
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
407
+ style: s.container
408
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
409
+ style: s.title
410
+ }, locale.selfie_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
411
+ style: s.subtitle
412
+ }, locale.selfie_instruction), preview ? /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(reactNative.Image, {
413
+ source: {
414
+ uri: preview
415
+ },
416
+ style: s.preview,
417
+ resizeMode: "cover"
418
+ }), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
419
+ style: s.btnPrimary,
420
+ onPress: ()=>onCapture(preview),
421
+ activeOpacity: 0.85
422
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
423
+ style: s.btnText
424
+ }, locale.selfie_next_btn)), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
425
+ style: s.btnSecondary,
426
+ onPress: ()=>setPreview(null),
427
+ activeOpacity: 0.75
428
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
429
+ style: s.btnSecondaryText
430
+ }, locale.selfie_retake_btn))) : /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(reactNative.View, {
431
+ style: s.viewport
432
+ }, /*#__PURE__*/ React.createElement(reactNativeVisionCamera.Camera, {
433
+ ref: cameraRef,
434
+ style: reactNative.StyleSheet.absoluteFill,
435
+ device: device,
436
+ isActive: true,
437
+ photo: true
438
+ }), /*#__PURE__*/ React.createElement(reactNative.View, {
439
+ style: s.ovalGuide,
440
+ pointerEvents: "none"
441
+ })), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
442
+ style: [
443
+ s.btnPrimary,
444
+ capturing && s.btnDisabled
445
+ ],
446
+ onPress: handleCapture,
447
+ disabled: capturing,
448
+ activeOpacity: 0.85
449
+ }, capturing ? /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
450
+ color: "#fff"
451
+ }) : /*#__PURE__*/ React.createElement(reactNative.Text, {
452
+ style: s.btnText
453
+ }, locale.selfie_capture_btn)), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
454
+ style: s.btnSecondary,
455
+ onPress: onBack,
456
+ activeOpacity: 0.75
457
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
458
+ style: s.btnSecondaryText
459
+ }, locale.doc_retake_btn))));
460
+ }
461
+ const styles$4 = (t)=>reactNative.StyleSheet.create({
462
+ container: {
463
+ padding: 24
464
+ },
465
+ title: {
466
+ fontSize: 20,
467
+ fontWeight: '700',
468
+ color: t.text,
469
+ marginBottom: 8
470
+ },
471
+ subtitle: {
472
+ fontSize: 14,
473
+ color: t.subtext,
474
+ lineHeight: 20,
475
+ marginBottom: 16
476
+ },
477
+ viewport: {
478
+ width: '100%',
479
+ aspectRatio: 3 / 4,
480
+ borderRadius: t.radius,
481
+ overflow: 'hidden',
482
+ backgroundColor: '#000',
483
+ marginBottom: 16,
484
+ position: 'relative',
485
+ alignSelf: 'center',
486
+ maxWidth: 280
487
+ },
488
+ ovalGuide: {
489
+ position: 'absolute',
490
+ top: '8%',
491
+ left: '12%',
492
+ right: '12%',
493
+ bottom: '8%',
494
+ borderWidth: 2,
495
+ borderColor: 'rgba(255,255,255,0.7)',
496
+ borderRadius: 999
497
+ },
498
+ preview: {
499
+ width: 240,
500
+ height: 320,
501
+ borderRadius: t.radius,
502
+ marginBottom: 16,
503
+ alignSelf: 'center',
504
+ borderWidth: 1,
505
+ borderColor: t.border
506
+ },
507
+ btnPrimary: {
508
+ backgroundColor: t.accent,
509
+ borderRadius: t.radius,
510
+ paddingVertical: 14,
511
+ alignItems: 'center',
512
+ marginBottom: 10
513
+ },
514
+ btnSecondary: {
515
+ backgroundColor: t.card,
516
+ borderRadius: t.radius,
517
+ paddingVertical: 14,
518
+ alignItems: 'center',
519
+ borderWidth: 1,
520
+ borderColor: t.border
521
+ },
522
+ btnDisabled: {
523
+ opacity: 0.6
524
+ },
525
+ btnText: {
526
+ color: '#fff',
527
+ fontSize: 15,
528
+ fontWeight: '600'
529
+ },
530
+ btnSecondaryText: {
531
+ color: t.text,
532
+ fontSize: 15,
533
+ fontWeight: '600'
534
+ }
535
+ });
536
+
537
+ function LivenessStep({ theme, locale, duration, onCapture, onBack }) {
538
+ const { cameraRef, device, startRecording, stopRecording } = useCamera('front');
539
+ const [recording, setRecording] = React.useState(false);
540
+ const [countdown, setCountdown] = React.useState(Math.ceil(duration / 1000));
541
+ const timerRef = React.useRef(null);
542
+ const pulseAnim = React.useRef(new reactNative.Animated.Value(1)).current;
543
+ const s = styles$3(theme);
544
+ // Pulse animation for recording indicator
545
+ React.useEffect(()=>{
546
+ if (recording) {
547
+ reactNative.Animated.loop(reactNative.Animated.sequence([
548
+ reactNative.Animated.timing(pulseAnim, {
549
+ toValue: 0.3,
550
+ duration: 600,
551
+ useNativeDriver: true
552
+ }),
553
+ reactNative.Animated.timing(pulseAnim, {
554
+ toValue: 1,
555
+ duration: 600,
556
+ useNativeDriver: true
557
+ })
558
+ ])).start();
559
+ } else {
560
+ pulseAnim.setValue(1);
561
+ }
562
+ }, [
563
+ recording,
564
+ pulseAnim
565
+ ]);
566
+ const handleStart = ()=>{
567
+ if (!cameraRef.current) return;
568
+ setRecording(true);
569
+ setCountdown(Math.ceil(duration / 1000));
570
+ startRecording((video)=>{
571
+ setRecording(false);
572
+ onCapture(video.uri);
573
+ }, ()=>setRecording(false));
574
+ // Countdown tick
575
+ let remaining = duration / 1000;
576
+ timerRef.current = setInterval(()=>{
577
+ remaining -= 0.1;
578
+ setCountdown(Math.max(0, Math.ceil(remaining)));
579
+ if (remaining <= 0) {
580
+ if (timerRef.current) clearInterval(timerRef.current);
581
+ stopRecording();
582
+ }
583
+ }, 100);
584
+ };
585
+ React.useEffect(()=>{
586
+ return ()=>{
587
+ if (timerRef.current) clearInterval(timerRef.current);
588
+ };
589
+ }, []);
590
+ if (!device) return /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
591
+ color: theme.accent,
592
+ style: {
593
+ margin: 40
594
+ }
595
+ });
596
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
597
+ style: s.container
598
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
599
+ style: s.title
600
+ }, locale.liveness_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
601
+ style: s.subtitle
602
+ }, recording ? locale.liveness_recording : locale.liveness_instruction), /*#__PURE__*/ React.createElement(reactNative.View, {
603
+ style: s.viewport
604
+ }, /*#__PURE__*/ React.createElement(reactNativeVisionCamera.Camera, {
605
+ ref: cameraRef,
606
+ style: reactNative.StyleSheet.absoluteFill,
607
+ device: device,
608
+ isActive: true,
609
+ video: true,
610
+ audio: true
611
+ }), /*#__PURE__*/ React.createElement(reactNative.View, {
612
+ style: s.ovalGuide,
613
+ pointerEvents: "none"
614
+ }), recording && /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(reactNative.View, {
615
+ style: s.recBadge
616
+ }, /*#__PURE__*/ React.createElement(reactNative.Animated.View, {
617
+ style: [
618
+ s.recDot,
619
+ {
620
+ opacity: pulseAnim
621
+ }
622
+ ]
623
+ }), /*#__PURE__*/ React.createElement(reactNative.Text, {
624
+ style: s.recText
625
+ }, "REC")), /*#__PURE__*/ React.createElement(reactNative.View, {
626
+ style: s.countdownBadge
627
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
628
+ style: s.countdownText
629
+ }, countdown)))), !recording && /*#__PURE__*/ React.createElement(React.Fragment, null, /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
630
+ style: s.btnPrimary,
631
+ onPress: handleStart,
632
+ activeOpacity: 0.85
633
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
634
+ style: s.btnText
635
+ }, locale.liveness_start_btn)), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
636
+ style: s.btnSecondary,
637
+ onPress: onBack,
638
+ activeOpacity: 0.75
639
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
640
+ style: s.btnSecondaryText
641
+ }, locale.selfie_retake_btn))));
642
+ }
643
+ const styles$3 = (t)=>reactNative.StyleSheet.create({
644
+ container: {
645
+ padding: 24
646
+ },
647
+ title: {
648
+ fontSize: 20,
649
+ fontWeight: '700',
650
+ color: t.text,
651
+ marginBottom: 8
652
+ },
653
+ subtitle: {
654
+ fontSize: 14,
655
+ color: t.subtext,
656
+ lineHeight: 20,
657
+ marginBottom: 16
658
+ },
659
+ viewport: {
660
+ width: '100%',
661
+ aspectRatio: 3 / 4,
662
+ borderRadius: t.radius,
663
+ overflow: 'hidden',
664
+ backgroundColor: '#000',
665
+ marginBottom: 16,
666
+ alignSelf: 'center',
667
+ maxWidth: 280,
668
+ position: 'relative'
669
+ },
670
+ ovalGuide: {
671
+ position: 'absolute',
672
+ top: '8%',
673
+ left: '12%',
674
+ right: '12%',
675
+ bottom: '8%',
676
+ borderWidth: 2,
677
+ borderColor: 'rgba(255,255,255,0.7)',
678
+ borderRadius: 999
679
+ },
680
+ recBadge: {
681
+ position: 'absolute',
682
+ top: 10,
683
+ left: 10,
684
+ flexDirection: 'row',
685
+ alignItems: 'center',
686
+ backgroundColor: 'rgba(220,38,38,0.85)',
687
+ borderRadius: 999,
688
+ paddingHorizontal: 10,
689
+ paddingVertical: 4,
690
+ gap: 6
691
+ },
692
+ recDot: {
693
+ width: 8,
694
+ height: 8,
695
+ borderRadius: 4,
696
+ backgroundColor: '#fff'
697
+ },
698
+ recText: {
699
+ color: '#fff',
700
+ fontSize: 12,
701
+ fontWeight: '700'
702
+ },
703
+ countdownBadge: {
704
+ position: 'absolute',
705
+ bottom: 12,
706
+ alignSelf: 'center',
707
+ width: 44,
708
+ height: 44,
709
+ borderRadius: 22,
710
+ backgroundColor: 'rgba(0,0,0,0.5)',
711
+ alignItems: 'center',
712
+ justifyContent: 'center'
713
+ },
714
+ countdownText: {
715
+ color: '#fff',
716
+ fontSize: 18,
717
+ fontWeight: '700'
718
+ },
719
+ btnPrimary: {
720
+ backgroundColor: t.accent,
721
+ borderRadius: t.radius,
722
+ paddingVertical: 14,
723
+ alignItems: 'center',
724
+ marginBottom: 10
725
+ },
726
+ btnSecondary: {
727
+ backgroundColor: t.card,
728
+ borderRadius: t.radius,
729
+ paddingVertical: 14,
730
+ alignItems: 'center',
731
+ borderWidth: 1,
732
+ borderColor: t.border
733
+ },
734
+ btnText: {
735
+ color: '#fff',
736
+ fontSize: 15,
737
+ fontWeight: '600'
738
+ },
739
+ btnSecondaryText: {
740
+ color: t.text,
741
+ fontSize: 15,
742
+ fontWeight: '600'
743
+ }
744
+ });
745
+
746
+ function ProcessingStep({ theme, locale }) {
747
+ const spinAnim = React.useRef(new reactNative.Animated.Value(0)).current;
748
+ const s = styles$2(theme);
749
+ React.useEffect(()=>{
750
+ reactNative.Animated.loop(reactNative.Animated.timing(spinAnim, {
751
+ toValue: 1,
752
+ duration: 800,
753
+ useNativeDriver: true
754
+ })).start();
755
+ }, [
756
+ spinAnim
757
+ ]);
758
+ const spin = spinAnim.interpolate({
759
+ inputRange: [
760
+ 0,
761
+ 1
762
+ ],
763
+ outputRange: [
764
+ '0deg',
765
+ '360deg'
766
+ ]
767
+ });
768
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
769
+ style: s.container
770
+ }, /*#__PURE__*/ React.createElement(reactNative.Animated.View, {
771
+ style: [
772
+ s.spinner,
773
+ {
774
+ transform: [
775
+ {
776
+ rotate: spin
777
+ }
778
+ ]
779
+ }
780
+ ]
781
+ }), /*#__PURE__*/ React.createElement(reactNative.Text, {
782
+ style: s.title
783
+ }, locale.processing_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
784
+ style: s.subtitle
785
+ }, locale.processing_subtitle));
786
+ }
787
+ const styles$2 = (t)=>reactNative.StyleSheet.create({
788
+ container: {
789
+ padding: 48,
790
+ alignItems: 'center',
791
+ gap: 16
792
+ },
793
+ spinner: {
794
+ width: 52,
795
+ height: 52,
796
+ borderRadius: 26,
797
+ borderWidth: 4,
798
+ borderColor: t.border,
799
+ borderTopColor: t.accent,
800
+ marginBottom: 8
801
+ },
802
+ title: {
803
+ fontSize: 18,
804
+ fontWeight: '700',
805
+ color: t.text,
806
+ textAlign: 'center'
807
+ },
808
+ subtitle: {
809
+ fontSize: 14,
810
+ color: t.subtext,
811
+ textAlign: 'center'
812
+ }
813
+ });
814
+
815
+ const ICONS = {
816
+ pass: '✅',
817
+ fail: '❌',
818
+ review: '⏳'
819
+ };
820
+ const ICON_BG = {
821
+ pass: '#d1fae5',
822
+ fail: '#fee2e2',
823
+ review: '#fef3c7'
824
+ };
825
+ function ResultStep({ theme, locale, result, error, onRetry }) {
826
+ const s = styles$1(theme);
827
+ if (!result) {
828
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
829
+ style: s.container
830
+ }, /*#__PURE__*/ React.createElement(reactNative.View, {
831
+ style: [
832
+ s.iconWrap,
833
+ {
834
+ backgroundColor: '#fee2e2'
835
+ }
836
+ ]
837
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
838
+ style: s.icon
839
+ }, "❌")), /*#__PURE__*/ React.createElement(reactNative.Text, {
840
+ style: s.title
841
+ }, locale.result_fail_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
842
+ style: s.body
843
+ }, error ?? locale.result_fail_body), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
844
+ style: s.btn,
845
+ onPress: onRetry,
846
+ activeOpacity: 0.85
847
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
848
+ style: s.btnText
849
+ }, locale.result_retry_btn)));
850
+ }
851
+ const titles = {
852
+ pass: locale.result_pass_title,
853
+ fail: locale.result_fail_title,
854
+ review: locale.result_review_title
855
+ };
856
+ const bodies = {
857
+ pass: locale.result_pass_body,
858
+ fail: locale.result_fail_body,
859
+ review: locale.result_review_body
860
+ };
861
+ return /*#__PURE__*/ React.createElement(reactNative.View, {
862
+ style: s.container
863
+ }, /*#__PURE__*/ React.createElement(reactNative.View, {
864
+ style: [
865
+ s.iconWrap,
866
+ {
867
+ backgroundColor: ICON_BG[result.verdict]
868
+ }
869
+ ]
870
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
871
+ style: s.icon
872
+ }, ICONS[result.verdict])), /*#__PURE__*/ React.createElement(reactNative.Text, {
873
+ style: s.title
874
+ }, titles[result.verdict]), /*#__PURE__*/ React.createElement(reactNative.Text, {
875
+ style: s.body
876
+ }, bodies[result.verdict]), result.verdict === 'fail' && /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
877
+ style: s.btn,
878
+ onPress: onRetry,
879
+ activeOpacity: 0.85
880
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
881
+ style: s.btnText
882
+ }, locale.result_retry_btn)));
883
+ }
884
+ const styles$1 = (t)=>reactNative.StyleSheet.create({
885
+ container: {
886
+ padding: 40,
887
+ alignItems: 'center',
888
+ gap: 12
889
+ },
890
+ iconWrap: {
891
+ width: 76,
892
+ height: 76,
893
+ borderRadius: 38,
894
+ alignItems: 'center',
895
+ justifyContent: 'center',
896
+ marginBottom: 8
897
+ },
898
+ icon: {
899
+ fontSize: 34
900
+ },
901
+ title: {
902
+ fontSize: 20,
903
+ fontWeight: '700',
904
+ color: t.text,
905
+ textAlign: 'center'
906
+ },
907
+ body: {
908
+ fontSize: 14,
909
+ color: t.subtext,
910
+ textAlign: 'center',
911
+ lineHeight: 21
912
+ },
913
+ btn: {
914
+ marginTop: 8,
915
+ backgroundColor: t.accent,
916
+ borderRadius: t.radius,
917
+ paddingVertical: 14,
918
+ paddingHorizontal: 32,
919
+ alignItems: 'center'
920
+ },
921
+ btnText: {
922
+ color: '#fff',
923
+ fontSize: 15,
924
+ fontWeight: '600'
925
+ }
926
+ });
927
+
928
+ const STEP_INDEX = {
929
+ intro: -1,
930
+ document: 0,
931
+ selfie: 1,
932
+ liveness: 2,
933
+ processing: 3,
934
+ result: 3
935
+ };
936
+ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference, sandbox = false, livenessDuration = 3000, documentTypes = [
937
+ 'national_id',
938
+ 'passport'
939
+ ], theme: themeProp, locale: localeProp, onCancel, onComplete, onError }) {
940
+ const [step, setStep] = React.useState('intro');
941
+ const [capture, setCapture] = React.useState({});
942
+ const [result, setResult] = React.useState(null);
943
+ const [error, setError] = React.useState(null);
944
+ const theme = React.useMemo(()=>resolveTheme(themeProp), [
945
+ themeProp
946
+ ]);
947
+ const locale = React.useMemo(()=>({
948
+ ...DEFAULT_LOCALE,
949
+ ...localeProp
950
+ }), [
951
+ localeProp
952
+ ]);
953
+ const { requestPermissions } = usePermissions();
954
+ const s = styles(theme);
955
+ const reset = React.useCallback(()=>{
956
+ setStep('intro');
957
+ setCapture({});
958
+ setResult(null);
959
+ setError(null);
960
+ }, []);
961
+ const goto = React.useCallback(async (next)=>{
962
+ if (next === 'document' || next === 'selfie' || next === 'liveness') {
963
+ const granted = await requestPermissions();
964
+ if (!granted) {
965
+ setError(locale.error_camera_denied);
966
+ return;
967
+ }
968
+ }
969
+ setStep(next);
970
+ if (next === 'processing') {
971
+ submit();
972
+ }
973
+ // eslint-disable-next-line react-hooks/exhaustive-deps
974
+ }, [
975
+ capture,
976
+ locale
977
+ ]);
978
+ const submit = React.useCallback(async ()=>{
979
+ const { documentUri, selfieUri, videoUri } = capture;
980
+ if (!documentUri || !selfieUri) {
981
+ setError('Missing captures — please restart.');
982
+ setStep('result');
983
+ return;
984
+ }
985
+ const base = (backendUrl ?? 'https://api.identity.kernaq.com/v1').replace(/\/$/, '');
986
+ const form = new FormData();
987
+ form.append('document', {
988
+ uri: documentUri,
989
+ name: 'document.jpg',
990
+ type: 'image/jpeg'
991
+ });
992
+ form.append('selfie', {
993
+ uri: selfieUri,
994
+ name: 'selfie.jpg',
995
+ type: 'image/jpeg'
996
+ });
997
+ if (videoUri) {
998
+ form.append('video', {
999
+ uri: videoUri,
1000
+ name: 'liveness.mp4',
1001
+ type: 'video/mp4'
1002
+ });
1003
+ }
1004
+ form.append('document_type', documentTypes[0] ?? 'national_id');
1005
+ form.append('country', country);
1006
+ form.append('reference', reference ?? `kq_${Date.now()}`);
1007
+ if (sandbox) form.append('sandbox', 'true');
1008
+ const headers = {
1009
+ 'Content-Type': 'multipart/form-data'
1010
+ };
1011
+ if (apiKey) headers['X-API-Key'] = apiKey;
1012
+ try {
1013
+ const res = await fetch(`${base}/verify`, {
1014
+ method: 'POST',
1015
+ headers,
1016
+ body: form
1017
+ });
1018
+ const body = await res.json();
1019
+ if (!res.ok) throw new Error(body['message'] ?? `HTTP ${res.status}`);
1020
+ const r = {
1021
+ verificationId: body['verification_id'],
1022
+ reference: body['reference'],
1023
+ verdict: body['verdict'],
1024
+ score: body['score'] ?? 0,
1025
+ faceMatch: body['face_match'] ?? false,
1026
+ isLive: body['is_live'] ?? false,
1027
+ documentFields: body['document_fields']
1028
+ };
1029
+ setResult(r);
1030
+ setStep('result');
1031
+ onComplete?.(r);
1032
+ } catch (err) {
1033
+ const msg = err.message;
1034
+ setError(`${locale.error_network} ${msg}`);
1035
+ setStep('result');
1036
+ onError?.({
1037
+ code: 'network_error',
1038
+ message: msg
1039
+ });
1040
+ }
1041
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1042
+ }, [
1043
+ capture
1044
+ ]);
1045
+ const handleCancel = ()=>{
1046
+ reset();
1047
+ onCancel?.();
1048
+ };
1049
+ const progressSteps = [
1050
+ 'document',
1051
+ 'selfie',
1052
+ 'liveness',
1053
+ 'processing'
1054
+ ];
1055
+ const currentIdx = STEP_INDEX[step];
1056
+ return /*#__PURE__*/ React.createElement(reactNative.Modal, {
1057
+ visible: visible,
1058
+ animationType: "slide",
1059
+ presentationStyle: "pageSheet",
1060
+ onRequestClose: handleCancel
1061
+ }, /*#__PURE__*/ React.createElement(reactNative.SafeAreaView, {
1062
+ style: [
1063
+ s.safeArea,
1064
+ {
1065
+ backgroundColor: theme.bg
1066
+ }
1067
+ ]
1068
+ }, /*#__PURE__*/ React.createElement(reactNative.View, {
1069
+ style: s.header
1070
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
1071
+ style: s.logo
1072
+ }, "Kernaq"), step !== 'processing' && /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
1073
+ onPress: handleCancel,
1074
+ style: s.closeBtn,
1075
+ hitSlop: {
1076
+ top: 8,
1077
+ bottom: 8,
1078
+ left: 8,
1079
+ right: 8
1080
+ }
1081
+ }, /*#__PURE__*/ React.createElement(reactNative.Text, {
1082
+ style: s.closeText
1083
+ }, "✕"))), currentIdx >= 0 && /*#__PURE__*/ React.createElement(reactNative.View, {
1084
+ style: s.progress
1085
+ }, progressSteps.map((_, i)=>/*#__PURE__*/ React.createElement(reactNative.View, {
1086
+ key: i,
1087
+ style: [
1088
+ s.progressDot,
1089
+ i < currentIdx && {
1090
+ backgroundColor: theme.accent,
1091
+ opacity: 0.4
1092
+ },
1093
+ i === currentIdx && {
1094
+ backgroundColor: theme.accent
1095
+ }
1096
+ ]
1097
+ }))), /*#__PURE__*/ React.createElement(reactNative.ScrollView, {
1098
+ style: s.scroll,
1099
+ contentContainerStyle: s.scrollContent,
1100
+ keyboardShouldPersistTaps: "handled",
1101
+ bounces: false
1102
+ }, step === 'intro' && /*#__PURE__*/ React.createElement(IntroStep, {
1103
+ theme: theme,
1104
+ locale: locale,
1105
+ onStart: ()=>goto('document')
1106
+ }), step === 'document' && /*#__PURE__*/ React.createElement(DocumentStep, {
1107
+ theme: theme,
1108
+ locale: locale,
1109
+ onCapture: (uri)=>{
1110
+ setCapture((c)=>({
1111
+ ...c,
1112
+ documentUri: uri
1113
+ }));
1114
+ goto('selfie');
1115
+ }
1116
+ }), step === 'selfie' && /*#__PURE__*/ React.createElement(SelfieStep, {
1117
+ theme: theme,
1118
+ locale: locale,
1119
+ onCapture: (uri)=>{
1120
+ setCapture((c)=>({
1121
+ ...c,
1122
+ selfieUri: uri
1123
+ }));
1124
+ goto('liveness');
1125
+ },
1126
+ onBack: ()=>setStep('document')
1127
+ }), step === 'liveness' && /*#__PURE__*/ React.createElement(LivenessStep, {
1128
+ theme: theme,
1129
+ locale: locale,
1130
+ duration: livenessDuration,
1131
+ onCapture: (uri)=>{
1132
+ setCapture((c)=>({
1133
+ ...c,
1134
+ videoUri: uri
1135
+ }));
1136
+ goto('processing');
1137
+ },
1138
+ onBack: ()=>setStep('selfie')
1139
+ }), step === 'processing' && /*#__PURE__*/ React.createElement(ProcessingStep, {
1140
+ theme: theme,
1141
+ locale: locale
1142
+ }), step === 'result' && /*#__PURE__*/ React.createElement(ResultStep, {
1143
+ theme: theme,
1144
+ locale: locale,
1145
+ result: result,
1146
+ error: error,
1147
+ onRetry: ()=>{
1148
+ reset();
1149
+ setStep('intro');
1150
+ }
1151
+ }))));
1152
+ }
1153
+ const styles = (t)=>reactNative.StyleSheet.create({
1154
+ safeArea: {
1155
+ flex: 1
1156
+ },
1157
+ header: {
1158
+ flexDirection: 'row',
1159
+ alignItems: 'center',
1160
+ justifyContent: 'space-between',
1161
+ paddingHorizontal: 20,
1162
+ paddingTop: reactNative.Platform.OS === 'android' ? 16 : 8,
1163
+ paddingBottom: 8
1164
+ },
1165
+ logo: {
1166
+ fontSize: 14,
1167
+ fontWeight: '700',
1168
+ color: t.accent,
1169
+ letterSpacing: 1.2,
1170
+ textTransform: 'uppercase'
1171
+ },
1172
+ closeBtn: {
1173
+ padding: 4
1174
+ },
1175
+ closeText: {
1176
+ fontSize: 18,
1177
+ color: t.subtext
1178
+ },
1179
+ progress: {
1180
+ flexDirection: 'row',
1181
+ gap: 6,
1182
+ paddingHorizontal: 20,
1183
+ paddingBottom: 8
1184
+ },
1185
+ progressDot: {
1186
+ flex: 1,
1187
+ height: 3,
1188
+ borderRadius: 999,
1189
+ backgroundColor: t.border
1190
+ },
1191
+ scroll: {
1192
+ flex: 1
1193
+ },
1194
+ scrollContent: {
1195
+ flexGrow: 1
1196
+ }
1197
+ });
1198
+
1199
+ exports.KernaqVerify = KernaqVerify;
1200
+ //# sourceMappingURL=index.js.map