@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.
@@ -0,0 +1,108 @@
1
+ import React from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
3
+ import type { ResolvedTheme } from '../theme'
4
+ import type { VerifyLocale, VerifyResult, VerifyVerdict } from '../types'
5
+
6
+ interface Props {
7
+ theme: ResolvedTheme
8
+ locale: VerifyLocale
9
+ result: VerifyResult | null
10
+ error: string | null
11
+ onRetry: () => void
12
+ }
13
+
14
+ const ICONS: Record<VerifyVerdict, string> = {
15
+ pass: '✅',
16
+ fail: '❌',
17
+ review: '⏳',
18
+ }
19
+
20
+ const ICON_BG: Record<VerifyVerdict, string> = {
21
+ pass: '#d1fae5',
22
+ fail: '#fee2e2',
23
+ review: '#fef3c7',
24
+ }
25
+
26
+ export function ResultStep({ theme, locale, result, error, onRetry }: Props) {
27
+ const s = styles(theme)
28
+
29
+ if (!result) {
30
+ return (
31
+ <View style={s.container}>
32
+ <View style={[s.iconWrap, { backgroundColor: '#fee2e2' }]}>
33
+ <Text style={s.icon}>❌</Text>
34
+ </View>
35
+ <Text style={s.title}>{locale.result_fail_title}</Text>
36
+ <Text style={s.body}>{error ?? locale.result_fail_body}</Text>
37
+ <TouchableOpacity style={s.btn} onPress={onRetry} activeOpacity={0.85}>
38
+ <Text style={s.btnText}>{locale.result_retry_btn}</Text>
39
+ </TouchableOpacity>
40
+ </View>
41
+ )
42
+ }
43
+
44
+ const titles: Record<VerifyVerdict, string> = {
45
+ pass: locale.result_pass_title,
46
+ fail: locale.result_fail_title,
47
+ review: locale.result_review_title,
48
+ }
49
+ const bodies: Record<VerifyVerdict, string> = {
50
+ pass: locale.result_pass_body,
51
+ fail: locale.result_fail_body,
52
+ review: locale.result_review_body,
53
+ }
54
+
55
+ return (
56
+ <View style={s.container}>
57
+ <View style={[s.iconWrap, { backgroundColor: ICON_BG[result.verdict] }]}>
58
+ <Text style={s.icon}>{ICONS[result.verdict]}</Text>
59
+ </View>
60
+ <Text style={s.title}>{titles[result.verdict]}</Text>
61
+ <Text style={s.body}>{bodies[result.verdict]}</Text>
62
+ {result.verdict === 'fail' && (
63
+ <TouchableOpacity style={s.btn} onPress={onRetry} activeOpacity={0.85}>
64
+ <Text style={s.btnText}>{locale.result_retry_btn}</Text>
65
+ </TouchableOpacity>
66
+ )}
67
+ </View>
68
+ )
69
+ }
70
+
71
+ const styles = (t: ResolvedTheme) =>
72
+ StyleSheet.create({
73
+ container: {
74
+ padding: 40,
75
+ alignItems: 'center',
76
+ gap: 12,
77
+ },
78
+ iconWrap: {
79
+ width: 76,
80
+ height: 76,
81
+ borderRadius: 38,
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ marginBottom: 8,
85
+ },
86
+ icon: { fontSize: 34 },
87
+ title: {
88
+ fontSize: 20,
89
+ fontWeight: '700',
90
+ color: t.text,
91
+ textAlign: 'center',
92
+ },
93
+ body: {
94
+ fontSize: 14,
95
+ color: t.subtext,
96
+ textAlign: 'center',
97
+ lineHeight: 21,
98
+ },
99
+ btn: {
100
+ marginTop: 8,
101
+ backgroundColor: t.accent,
102
+ borderRadius: t.radius,
103
+ paddingVertical: 14,
104
+ paddingHorizontal: 32,
105
+ alignItems: 'center',
106
+ },
107
+ btnText: { color: '#fff', fontSize: 15, fontWeight: '600' },
108
+ })
@@ -0,0 +1,138 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ Image,
7
+ StyleSheet,
8
+ ActivityIndicator,
9
+ } from 'react-native'
10
+ import { Camera } from 'react-native-vision-camera'
11
+ import { useCamera } from '../hooks/useCamera'
12
+ import type { ResolvedTheme } from '../theme'
13
+ import type { VerifyLocale } from '../types'
14
+
15
+ interface Props {
16
+ theme: ResolvedTheme
17
+ locale: VerifyLocale
18
+ onCapture: (uri: string) => void
19
+ onBack: () => void
20
+ }
21
+
22
+ export function SelfieStep({ theme, locale, onCapture, onBack }: Props) {
23
+ const { cameraRef, device, capturePhoto } = useCamera('front')
24
+ const [preview, setPreview] = React.useState<string | null>(null)
25
+ const [capturing, setCapturing] = React.useState(false)
26
+ const s = styles(theme)
27
+
28
+ const handleCapture = async () => {
29
+ setCapturing(true)
30
+ const result = await capturePhoto()
31
+ setCapturing(false)
32
+ if (result) setPreview(result.uri)
33
+ }
34
+
35
+ if (!device) return <ActivityIndicator color={theme.accent} style={{ margin: 40 }} />
36
+
37
+ return (
38
+ <View style={s.container}>
39
+ <Text style={s.title}>{locale.selfie_title}</Text>
40
+ <Text style={s.subtitle}>{locale.selfie_instruction}</Text>
41
+
42
+ {preview ? (
43
+ <>
44
+ <Image source={{ uri: preview }} style={s.preview} resizeMode="cover" />
45
+ <TouchableOpacity style={s.btnPrimary} onPress={() => onCapture(preview)} activeOpacity={0.85}>
46
+ <Text style={s.btnText}>{locale.selfie_next_btn}</Text>
47
+ </TouchableOpacity>
48
+ <TouchableOpacity style={s.btnSecondary} onPress={() => setPreview(null)} activeOpacity={0.75}>
49
+ <Text style={s.btnSecondaryText}>{locale.selfie_retake_btn}</Text>
50
+ </TouchableOpacity>
51
+ </>
52
+ ) : (
53
+ <>
54
+ <View style={s.viewport}>
55
+ <Camera
56
+ ref={cameraRef}
57
+ style={StyleSheet.absoluteFill}
58
+ device={device}
59
+ isActive={true}
60
+ photo={true}
61
+ />
62
+ {/* Oval face guide */}
63
+ <View style={s.ovalGuide} pointerEvents="none" />
64
+ </View>
65
+ <TouchableOpacity
66
+ style={[s.btnPrimary, capturing && s.btnDisabled]}
67
+ onPress={handleCapture}
68
+ disabled={capturing}
69
+ activeOpacity={0.85}
70
+ >
71
+ {capturing
72
+ ? <ActivityIndicator color="#fff" />
73
+ : <Text style={s.btnText}>{locale.selfie_capture_btn}</Text>
74
+ }
75
+ </TouchableOpacity>
76
+ <TouchableOpacity style={s.btnSecondary} onPress={onBack} activeOpacity={0.75}>
77
+ <Text style={s.btnSecondaryText}>{locale.doc_retake_btn}</Text>
78
+ </TouchableOpacity>
79
+ </>
80
+ )}
81
+ </View>
82
+ )
83
+ }
84
+
85
+ const styles = (t: ResolvedTheme) =>
86
+ StyleSheet.create({
87
+ container: { padding: 24 },
88
+ title: { fontSize: 20, fontWeight: '700', color: t.text, marginBottom: 8 },
89
+ subtitle: { fontSize: 14, color: t.subtext, lineHeight: 20, marginBottom: 16 },
90
+ viewport: {
91
+ width: '100%',
92
+ aspectRatio: 3 / 4,
93
+ borderRadius: t.radius,
94
+ overflow: 'hidden',
95
+ backgroundColor: '#000',
96
+ marginBottom: 16,
97
+ position: 'relative',
98
+ alignSelf: 'center',
99
+ maxWidth: 280,
100
+ },
101
+ ovalGuide: {
102
+ position: 'absolute',
103
+ top: '8%',
104
+ left: '12%',
105
+ right: '12%',
106
+ bottom: '8%',
107
+ borderWidth: 2,
108
+ borderColor: 'rgba(255,255,255,0.7)',
109
+ borderRadius: 999,
110
+ },
111
+ preview: {
112
+ width: 240,
113
+ height: 320,
114
+ borderRadius: t.radius,
115
+ marginBottom: 16,
116
+ alignSelf: 'center',
117
+ borderWidth: 1,
118
+ borderColor: t.border,
119
+ },
120
+ btnPrimary: {
121
+ backgroundColor: t.accent,
122
+ borderRadius: t.radius,
123
+ paddingVertical: 14,
124
+ alignItems: 'center',
125
+ marginBottom: 10,
126
+ },
127
+ btnSecondary: {
128
+ backgroundColor: t.card,
129
+ borderRadius: t.radius,
130
+ paddingVertical: 14,
131
+ alignItems: 'center',
132
+ borderWidth: 1,
133
+ borderColor: t.border,
134
+ },
135
+ btnDisabled: { opacity: 0.6 },
136
+ btnText: { color: '#fff', fontSize: 15, fontWeight: '600' },
137
+ btnSecondaryText: { color: t.text, fontSize: 15, fontWeight: '600' },
138
+ })
package/src/theme.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { VerifyTheme } from './types'
2
+
3
+ export interface ResolvedTheme {
4
+ accent: string
5
+ bg: string
6
+ text: string
7
+ subtext: string
8
+ border: string
9
+ card: string
10
+ radius: number
11
+ }
12
+
13
+ export function resolveTheme(theme?: VerifyTheme): ResolvedTheme {
14
+ const dark = theme?.mode === 'dark'
15
+ return {
16
+ accent: theme?.accentColor ?? '#111827',
17
+ bg: theme?.backgroundColor ?? (dark ? '#1a1a2e' : '#ffffff'),
18
+ text: theme?.textColor ?? (dark ? '#f9fafb' : '#111827'),
19
+ subtext: theme?.subtextColor ?? (dark ? '#9ca3af' : '#6b7280'),
20
+ border: theme?.borderColor ?? (dark ? '#374151' : '#e5e7eb'),
21
+ card: theme?.cardColor ?? (dark ? '#111827' : '#f9fafb'),
22
+ radius: theme?.borderRadius ?? 12,
23
+ }
24
+ }
package/src/types.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @kernaq/verify-react-native — public types
3
+ */
4
+
5
+ export type VerifyStep =
6
+ | 'intro'
7
+ | 'document'
8
+ | 'selfie'
9
+ | 'liveness'
10
+ | 'processing'
11
+ | 'result'
12
+
13
+ export type DocumentType =
14
+ | 'national_id'
15
+ | 'passport'
16
+ | 'driver_license'
17
+ | 'alien_card'
18
+
19
+ export type VerifyVerdict = 'pass' | 'fail' | 'review'
20
+
21
+ export interface VerifyResult {
22
+ verificationId: string
23
+ reference: string
24
+ verdict: VerifyVerdict
25
+ score: number
26
+ faceMatch: boolean
27
+ isLive: boolean
28
+ documentFields?: Record<string, string>
29
+ }
30
+
31
+ export interface VerifyTheme {
32
+ /** Primary accent colour. Default: #111827 */
33
+ accentColor?: string
34
+ /** Modal background. Default: #ffffff */
35
+ backgroundColor?: string
36
+ /** Primary text colour. Default: #111827 */
37
+ textColor?: string
38
+ /** Subtle text colour. Default: #6b7280 */
39
+ subtextColor?: string
40
+ /** Border/divider colour. Default: #e5e7eb */
41
+ borderColor?: string
42
+ /** Card/surface colour. Default: #f9fafb */
43
+ cardColor?: string
44
+ /** Border radius for buttons and cards. Default: 12 */
45
+ borderRadius?: number
46
+ /** 'light' | 'dark' — overrides bg/text when set */
47
+ mode?: 'light' | 'dark'
48
+ }
49
+
50
+ export interface VerifyLocale {
51
+ intro_title: string
52
+ intro_body: string
53
+ intro_cta: string
54
+ doc_title: string
55
+ doc_instruction: string
56
+ doc_capture_btn: string
57
+ doc_retake_btn: string
58
+ doc_next_btn: string
59
+ selfie_title: string
60
+ selfie_instruction: string
61
+ selfie_capture_btn: string
62
+ selfie_retake_btn: string
63
+ selfie_next_btn: string
64
+ liveness_title: string
65
+ liveness_instruction: string
66
+ liveness_start_btn: string
67
+ liveness_recording: string
68
+ processing_title: string
69
+ processing_subtitle: string
70
+ result_pass_title: string
71
+ result_pass_body: string
72
+ result_fail_title: string
73
+ result_fail_body: string
74
+ result_review_title: string
75
+ result_review_body: string
76
+ result_retry_btn: string
77
+ error_camera_denied: string
78
+ error_quality: string
79
+ error_network: string
80
+ }
81
+
82
+ export interface VerifyConfig {
83
+ /**
84
+ * Your Kernaq publishable API key (k_test_... or k_live_...).
85
+ * For production use a backend proxy and pass backendUrl instead.
86
+ */
87
+ apiKey?: string
88
+
89
+ /**
90
+ * URL of your backend proxy that forwards to the Kernaq Identity API.
91
+ * POST /verify with multipart/form-data.
92
+ */
93
+ backendUrl?: string
94
+
95
+ /** Document types the user can select. Default: ['national_id', 'passport'] */
96
+ documentTypes?: DocumentType[]
97
+
98
+ /** Country code (ISO 3166-1 alpha-3). Default: 'KEN' */
99
+ country?: string
100
+
101
+ /** Your internal reference ID for this user/session */
102
+ reference?: string
103
+
104
+ /** Use sandbox mode — no billing, test responses */
105
+ sandbox?: boolean
106
+
107
+ /** Liveness recording duration in ms. Default: 3000 */
108
+ livenessDuration?: number
109
+
110
+ /** Theming */
111
+ theme?: VerifyTheme
112
+
113
+ /** Override any displayed string */
114
+ locale?: Partial<VerifyLocale>
115
+
116
+ /** Called when the user dismisses without completing */
117
+ onCancel?: () => void
118
+
119
+ /** Called when verification is complete */
120
+ onComplete?: (result: VerifyResult) => void
121
+
122
+ /** Called on unrecoverable error */
123
+ onError?: (error: { code: string; message: string }) => void
124
+ }