@kernaq/verify-react-native 2.0.0 → 2.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/README.md +1 -1
- package/dist/index.d.ts +95 -50
- package/dist/index.esm.js +1171 -580
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1169 -576
- package/dist/index.js.map +1 -1
- package/package.json +2 -9
- package/src/KernaqVerify.tsx +172 -176
- package/src/index.ts +9 -4
- package/src/locale.ts +32 -29
- package/src/quality.ts +180 -0
- package/src/steps/DocumentStep.tsx +214 -77
- package/src/steps/IntroStep.tsx +58 -37
- package/src/steps/LivenessStep.tsx +225 -125
- package/src/steps/ProcessingStep.tsx +10 -34
- package/src/steps/ResultStep.tsx +99 -35
- package/src/steps/SelfieStep.tsx +165 -96
- package/src/theme.ts +22 -14
- package/src/types.ts +27 -51
package/src/KernaqVerify.tsx
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* <KernaqVerify /> — drop-in identity verification modal for React Native.
|
|
2
|
+
* <KernaqVerify /> — drop-in identity verification modal for React Native (v2).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Matches @kernaq/verify web v2.0.0:
|
|
5
|
+
* - No header / logo
|
|
6
|
+
* - No progress bar
|
|
7
|
+
* - No cancel/close button — user must complete the flow
|
|
8
|
+
* - Front + back document capture
|
|
9
|
+
* - Auto-capture selfie with guidance pill
|
|
10
|
+
* - Randomised liveness tasks with in-viewport overlay
|
|
11
|
+
* - Configurable steps prop
|
|
12
|
+
* - "Secured by Kernaq" footer only
|
|
6
13
|
*
|
|
14
|
+
* Usage:
|
|
7
15
|
* <KernaqVerify
|
|
8
|
-
* visible={
|
|
9
|
-
*
|
|
16
|
+
* visible={show}
|
|
17
|
+
* documentType="national_id"
|
|
10
18
|
* country="KEN"
|
|
11
19
|
* reference={userId}
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* steps={['document', 'selfie', 'liveness']}
|
|
21
|
+
* onComplete={(r) => console.log(r.verdict)}
|
|
14
22
|
* />
|
|
15
23
|
*/
|
|
16
|
-
import React, { useState, useCallback, useMemo } from 'react'
|
|
24
|
+
import React, { useState, useCallback, useMemo, useRef } from 'react'
|
|
17
25
|
import {
|
|
18
26
|
Modal,
|
|
19
27
|
View,
|
|
20
28
|
Text,
|
|
21
|
-
TouchableOpacity,
|
|
22
|
-
ScrollView,
|
|
23
29
|
StyleSheet,
|
|
24
30
|
SafeAreaView,
|
|
25
31
|
Platform,
|
|
@@ -27,130 +33,147 @@ import {
|
|
|
27
33
|
import { DEFAULT_LOCALE } from './locale'
|
|
28
34
|
import { resolveTheme } from './theme'
|
|
29
35
|
import { usePermissions } from './hooks/usePermissions'
|
|
30
|
-
import { IntroStep }
|
|
31
|
-
import { DocumentStep }
|
|
32
|
-
import { SelfieStep }
|
|
33
|
-
import { LivenessStep }
|
|
36
|
+
import { IntroStep } from './steps/IntroStep'
|
|
37
|
+
import { DocumentStep } from './steps/DocumentStep'
|
|
38
|
+
import { SelfieStep } from './steps/SelfieStep'
|
|
39
|
+
import { LivenessStep } from './steps/LivenessStep'
|
|
34
40
|
import { ProcessingStep } from './steps/ProcessingStep'
|
|
35
|
-
import { ResultStep }
|
|
36
|
-
import type {
|
|
41
|
+
import { ResultStep } from './steps/ResultStep'
|
|
42
|
+
import type {
|
|
43
|
+
VerifyConfig, VerifyStep, VerifyResult, VerifyLocale, VerifyStepName,
|
|
44
|
+
} from './types'
|
|
37
45
|
|
|
38
46
|
interface Props extends VerifyConfig {
|
|
39
47
|
visible: boolean
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
type CaptureState = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const STEP_INDEX: Record<VerifyStep, number> = {
|
|
49
|
-
intro: -1,
|
|
50
|
-
document: 0,
|
|
51
|
-
selfie: 1,
|
|
52
|
-
liveness: 2,
|
|
53
|
-
processing: 3,
|
|
54
|
-
result: 3,
|
|
51
|
+
docFrontUri?: string
|
|
52
|
+
docBackUri?: string
|
|
53
|
+
selfieUri?: string
|
|
54
|
+
videoUri?: string
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function KernaqVerify({
|
|
58
58
|
visible,
|
|
59
59
|
apiKey,
|
|
60
60
|
backendUrl,
|
|
61
|
-
country
|
|
61
|
+
country = 'KEN',
|
|
62
62
|
reference,
|
|
63
|
-
sandbox
|
|
63
|
+
sandbox = false,
|
|
64
64
|
livenessDuration = 3000,
|
|
65
|
-
|
|
65
|
+
livenessTaskCount = 2,
|
|
66
|
+
documentType = 'national_id',
|
|
67
|
+
steps: stepsProp,
|
|
66
68
|
theme: themeProp,
|
|
67
69
|
locale: localeProp,
|
|
68
|
-
onCancel,
|
|
69
70
|
onComplete,
|
|
70
71
|
onError,
|
|
71
72
|
}: Props) {
|
|
72
|
-
const [step,
|
|
73
|
+
const [step, setStep] = useState<VerifyStep>('intro')
|
|
73
74
|
const [capture, setCapture] = useState<CaptureState>({})
|
|
74
|
-
const [result,
|
|
75
|
-
const [error,
|
|
75
|
+
const [result, setResult] = useState<VerifyResult | null>(null)
|
|
76
|
+
const [error, setError] = useState<string | null>(null)
|
|
77
|
+
|
|
78
|
+
// Mirror capture in a ref so submit() always reads the latest value even
|
|
79
|
+
// when called from inside a stale closure (e.g. goto → submit chain).
|
|
80
|
+
const captureRef = useRef<CaptureState>({})
|
|
81
|
+
const setCaptureSync = (updater: (prev: CaptureState) => CaptureState) => {
|
|
82
|
+
setCapture((prev: CaptureState) => {
|
|
83
|
+
const next = updater(prev)
|
|
84
|
+
captureRef.current = next
|
|
85
|
+
return next
|
|
86
|
+
})
|
|
87
|
+
}
|
|
76
88
|
|
|
77
89
|
const theme = useMemo(() => resolveTheme(themeProp), [themeProp])
|
|
78
|
-
const locale: VerifyLocale = useMemo(
|
|
79
|
-
() => ({ ...DEFAULT_LOCALE, ...localeProp }),
|
|
80
|
-
[localeProp]
|
|
81
|
-
)
|
|
90
|
+
const locale: VerifyLocale = useMemo(() => ({ ...DEFAULT_LOCALE, ...localeProp }), [localeProp])
|
|
82
91
|
const { requestPermissions } = usePermissions()
|
|
83
92
|
const s = styles(theme)
|
|
84
93
|
|
|
94
|
+
// Build the ordered flow from the steps prop
|
|
95
|
+
const flow = useMemo((): VerifyStep[] => {
|
|
96
|
+
const wanted = stepsProp ?? ['document', 'selfie', 'liveness'] as VerifyStepName[]
|
|
97
|
+
const out: VerifyStep[] = []
|
|
98
|
+
if (wanted.includes('document')) { out.push('doc-front'); out.push('doc-back') }
|
|
99
|
+
if (wanted.includes('selfie')) out.push('selfie')
|
|
100
|
+
if (wanted.includes('liveness')) out.push('liveness')
|
|
101
|
+
out.push('processing')
|
|
102
|
+
return out
|
|
103
|
+
}, [stepsProp])
|
|
104
|
+
|
|
105
|
+
const next = useCallback((current: VerifyStep) => {
|
|
106
|
+
const idx = flow.indexOf(current)
|
|
107
|
+
const nextStep = flow[idx + 1] ?? 'processing'
|
|
108
|
+
goto(nextStep)
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
}, [flow])
|
|
111
|
+
|
|
112
|
+
const goto = useCallback(async (s: VerifyStep) => {
|
|
113
|
+
if (s === 'doc-front' || s === 'doc-back' || s === 'selfie' || s === 'liveness') {
|
|
114
|
+
const granted = await requestPermissions()
|
|
115
|
+
if (!granted) { setError(locale.error_camera_denied); return }
|
|
116
|
+
}
|
|
117
|
+
setStep(s)
|
|
118
|
+
if (s === 'processing') submit()
|
|
119
|
+
// submit is stable (no deps that change); locale/requestPermissions are stable refs
|
|
120
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
121
|
+
}, [locale])
|
|
122
|
+
|
|
85
123
|
const reset = useCallback(() => {
|
|
86
124
|
setStep('intro')
|
|
125
|
+
captureRef.current = {}
|
|
87
126
|
setCapture({})
|
|
88
127
|
setResult(null)
|
|
89
128
|
setError(null)
|
|
90
129
|
}, [])
|
|
91
130
|
|
|
92
|
-
const goto = useCallback(async (next: VerifyStep) => {
|
|
93
|
-
if (next === 'document' || next === 'selfie' || next === 'liveness') {
|
|
94
|
-
const granted = await requestPermissions()
|
|
95
|
-
if (!granted) {
|
|
96
|
-
setError(locale.error_camera_denied)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
setStep(next)
|
|
101
|
-
if (next === 'processing') {
|
|
102
|
-
submit()
|
|
103
|
-
}
|
|
104
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
|
-
}, [capture, locale])
|
|
106
|
-
|
|
107
131
|
const submit = useCallback(async () => {
|
|
108
|
-
const {
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
const { docFrontUri, docBackUri, selfieUri, videoUri } = captureRef.current
|
|
133
|
+
const steps = stepsProp ?? ['document', 'selfie', 'liveness'] as VerifyStepName[]
|
|
134
|
+
if (steps.includes('document') && !docFrontUri) {
|
|
135
|
+
setError('Document capture is missing — please restart.')
|
|
136
|
+
setStep('result')
|
|
137
|
+
onError?.({ code: 'capture_missing', message: 'Document capture missing' })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (steps.includes('selfie') && !selfieUri) {
|
|
141
|
+
setError('Selfie capture is missing — please restart.')
|
|
111
142
|
setStep('result')
|
|
143
|
+
onError?.({ code: 'capture_missing', message: 'Selfie capture missing' })
|
|
112
144
|
return
|
|
113
145
|
}
|
|
114
146
|
|
|
115
147
|
const base = (backendUrl ?? 'https://api.identity.kernaq.com/v1').replace(/\/$/, '')
|
|
148
|
+
const endpoint = sandbox ? '/verify/sandbox' : '/verify'
|
|
116
149
|
const form = new FormData()
|
|
117
150
|
|
|
118
|
-
|
|
119
|
-
uri:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
uri: selfieUri,
|
|
126
|
-
|
|
127
|
-
type: 'image/jpeg',
|
|
128
|
-
} as unknown as Blob)
|
|
129
|
-
|
|
151
|
+
if (docFrontUri) {
|
|
152
|
+
form.append('doc_front', { uri: docFrontUri, name: 'doc_front.jpg', type: 'image/jpeg' } as any)
|
|
153
|
+
}
|
|
154
|
+
if (docBackUri) {
|
|
155
|
+
form.append('doc_back', { uri: docBackUri, name: 'doc_back.jpg', type: 'image/jpeg' } as any)
|
|
156
|
+
}
|
|
157
|
+
if (selfieUri) {
|
|
158
|
+
form.append('selfie', { uri: selfieUri, name: 'selfie.jpg', type: 'image/jpeg' } as any)
|
|
159
|
+
}
|
|
130
160
|
if (videoUri) {
|
|
131
|
-
form.append('video', {
|
|
132
|
-
uri: videoUri,
|
|
133
|
-
name: 'liveness.mp4',
|
|
134
|
-
type: 'video/mp4',
|
|
135
|
-
} as unknown as Blob)
|
|
161
|
+
form.append('video', { uri: videoUri, name: 'liveness.mp4', type: 'video/mp4' } as any)
|
|
136
162
|
}
|
|
137
163
|
|
|
138
|
-
form.append('document_type',
|
|
139
|
-
form.append('country',
|
|
140
|
-
form.append('reference',
|
|
141
|
-
|
|
164
|
+
form.append('document_type', documentType)
|
|
165
|
+
form.append('country', country)
|
|
166
|
+
form.append('reference', reference ?? `kq_${Date.now()}`)
|
|
167
|
+
// sandbox is handled via URL routing above — no form field needed
|
|
142
168
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
169
|
+
// Do NOT set Content-Type manually — RN fetch will set multipart/form-data with
|
|
170
|
+
// the correct boundary automatically. Setting it manually omits the boundary and
|
|
171
|
+
// causes the server to reject the request.
|
|
172
|
+
const headers: Record<string, string> = {}
|
|
146
173
|
if (apiKey) headers['X-API-Key'] = apiKey
|
|
147
174
|
|
|
148
175
|
try {
|
|
149
|
-
const res
|
|
150
|
-
method: 'POST',
|
|
151
|
-
headers,
|
|
152
|
-
body: form,
|
|
153
|
-
})
|
|
176
|
+
const res = await fetch(`${base}${endpoint}`, { method: 'POST', headers, body: form })
|
|
154
177
|
const body = (await res.json()) as Record<string, unknown>
|
|
155
178
|
if (!res.ok) throw new Error((body['message'] as string) ?? `HTTP ${res.status}`)
|
|
156
179
|
|
|
@@ -169,117 +192,110 @@ export function KernaqVerify({
|
|
|
169
192
|
} catch (err) {
|
|
170
193
|
const msg = (err as Error).message
|
|
171
194
|
setError(`${locale.error_network} ${msg}`)
|
|
195
|
+
setResult(null)
|
|
172
196
|
setStep('result')
|
|
173
197
|
onError?.({ code: 'network_error', message: msg })
|
|
174
198
|
}
|
|
175
199
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
176
|
-
}, [
|
|
177
|
-
|
|
178
|
-
const handleCancel = () => {
|
|
179
|
-
reset()
|
|
180
|
-
onCancel?.()
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const progressSteps: VerifyStep[] = ['document', 'selfie', 'liveness', 'processing']
|
|
184
|
-
const currentIdx = STEP_INDEX[step]
|
|
200
|
+
}, []) // no deps — always reads from captureRef.current, never from stale closure
|
|
185
201
|
|
|
186
202
|
return (
|
|
187
203
|
<Modal
|
|
188
204
|
visible={visible}
|
|
189
205
|
animationType="slide"
|
|
190
206
|
presentationStyle="pageSheet"
|
|
191
|
-
onRequestClose={
|
|
207
|
+
onRequestClose={() => {/* intentionally empty — user cannot dismiss */}}
|
|
192
208
|
>
|
|
193
|
-
<SafeAreaView style={[s.
|
|
194
|
-
{/* Header */}
|
|
195
|
-
<View style={s.header}>
|
|
196
|
-
<Text style={s.logo}>Kernaq</Text>
|
|
197
|
-
{step !== 'processing' && (
|
|
198
|
-
<TouchableOpacity onPress={handleCancel} style={s.closeBtn} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
199
|
-
<Text style={s.closeText}>✕</Text>
|
|
200
|
-
</TouchableOpacity>
|
|
201
|
-
)}
|
|
202
|
-
</View>
|
|
203
|
-
|
|
204
|
-
{/* Progress bar */}
|
|
205
|
-
{currentIdx >= 0 && (
|
|
206
|
-
<View style={s.progress}>
|
|
207
|
-
{progressSteps.map((_, i) => (
|
|
208
|
-
<View
|
|
209
|
-
key={i}
|
|
210
|
-
style={[
|
|
211
|
-
s.progressDot,
|
|
212
|
-
i < currentIdx && { backgroundColor: theme.accent, opacity: 0.4 },
|
|
213
|
-
i === currentIdx && { backgroundColor: theme.accent },
|
|
214
|
-
]}
|
|
215
|
-
/>
|
|
216
|
-
))}
|
|
217
|
-
</View>
|
|
218
|
-
)}
|
|
209
|
+
<SafeAreaView style={[s.safe, { backgroundColor: theme.bg }]}>
|
|
219
210
|
|
|
220
|
-
{/*
|
|
221
|
-
<
|
|
222
|
-
style={s.scroll}
|
|
223
|
-
contentContainerStyle={s.scrollContent}
|
|
224
|
-
keyboardShouldPersistTaps="handled"
|
|
225
|
-
bounces={false}
|
|
226
|
-
>
|
|
211
|
+
{/* ── Content ── */}
|
|
212
|
+
<View style={s.body}>
|
|
227
213
|
{step === 'intro' && (
|
|
228
214
|
<IntroStep
|
|
229
215
|
theme={theme}
|
|
230
216
|
locale={locale}
|
|
231
|
-
onStart={() =>
|
|
217
|
+
onStart={() => {
|
|
218
|
+
const first = flow[0]
|
|
219
|
+
if (first) goto(first)
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{step === 'doc-front' && (
|
|
225
|
+
<DocumentStep
|
|
226
|
+
theme={theme}
|
|
227
|
+
locale={locale}
|
|
228
|
+
side="front"
|
|
229
|
+
onCapture={(uri) => {
|
|
230
|
+
setCaptureSync(c => ({ ...c, docFrontUri: uri }))
|
|
231
|
+
next('doc-front')
|
|
232
|
+
}}
|
|
232
233
|
/>
|
|
233
234
|
)}
|
|
234
|
-
|
|
235
|
+
|
|
236
|
+
{step === 'doc-back' && (
|
|
235
237
|
<DocumentStep
|
|
236
238
|
theme={theme}
|
|
237
239
|
locale={locale}
|
|
240
|
+
side="back"
|
|
241
|
+
frontUri={capture.docFrontUri}
|
|
238
242
|
onCapture={(uri) => {
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
setCaptureSync(c => ({ ...c, docBackUri: uri }))
|
|
244
|
+
next('doc-back')
|
|
245
|
+
}}
|
|
246
|
+
onRetakeFront={() => {
|
|
247
|
+
setCaptureSync(c => ({ ...c, docFrontUri: undefined }))
|
|
248
|
+
goto('doc-front')
|
|
241
249
|
}}
|
|
242
250
|
/>
|
|
243
251
|
)}
|
|
252
|
+
|
|
244
253
|
{step === 'selfie' && (
|
|
245
254
|
<SelfieStep
|
|
246
255
|
theme={theme}
|
|
247
256
|
locale={locale}
|
|
248
257
|
onCapture={(uri) => {
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
setCaptureSync(c => ({ ...c, selfieUri: uri }))
|
|
259
|
+
next('selfie')
|
|
251
260
|
}}
|
|
252
|
-
onBack={() => setStep('document')}
|
|
253
261
|
/>
|
|
254
262
|
)}
|
|
263
|
+
|
|
255
264
|
{step === 'liveness' && (
|
|
256
265
|
<LivenessStep
|
|
257
266
|
theme={theme}
|
|
258
267
|
locale={locale}
|
|
259
268
|
duration={livenessDuration}
|
|
269
|
+
taskCount={livenessTaskCount}
|
|
260
270
|
onCapture={(uri) => {
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
setCaptureSync(c => ({ ...c, videoUri: uri }))
|
|
272
|
+
next('liveness')
|
|
263
273
|
}}
|
|
264
|
-
onBack={() => setStep('selfie')}
|
|
265
274
|
/>
|
|
266
275
|
)}
|
|
276
|
+
|
|
267
277
|
{step === 'processing' && (
|
|
268
278
|
<ProcessingStep theme={theme} locale={locale} />
|
|
269
279
|
)}
|
|
280
|
+
|
|
270
281
|
{step === 'result' && (
|
|
271
282
|
<ResultStep
|
|
272
283
|
theme={theme}
|
|
273
284
|
locale={locale}
|
|
274
285
|
result={result}
|
|
275
286
|
error={error}
|
|
276
|
-
onRetry={() => {
|
|
277
|
-
reset()
|
|
278
|
-
setStep('intro')
|
|
279
|
-
}}
|
|
287
|
+
onRetry={() => { reset(); goto('intro') }}
|
|
280
288
|
/>
|
|
281
289
|
)}
|
|
282
|
-
</
|
|
290
|
+
</View>
|
|
291
|
+
|
|
292
|
+
{/* ── Footer — Secured by Kernaq ── */}
|
|
293
|
+
{step !== 'processing' && (
|
|
294
|
+
<View style={s.footer}>
|
|
295
|
+
<Text style={s.footerText}>Secured by </Text>
|
|
296
|
+
<Text style={[s.footerText, { fontWeight: '600', color: theme.text }]}>Kernaq</Text>
|
|
297
|
+
</View>
|
|
298
|
+
)}
|
|
283
299
|
</SafeAreaView>
|
|
284
300
|
</Modal>
|
|
285
301
|
)
|
|
@@ -287,43 +303,23 @@ export function KernaqVerify({
|
|
|
287
303
|
|
|
288
304
|
const styles = (t: ReturnType<typeof resolveTheme>) =>
|
|
289
305
|
StyleSheet.create({
|
|
290
|
-
|
|
306
|
+
safe: {
|
|
291
307
|
flex: 1,
|
|
292
308
|
},
|
|
293
|
-
|
|
309
|
+
body: {
|
|
310
|
+
flex: 1,
|
|
311
|
+
},
|
|
312
|
+
footer: {
|
|
294
313
|
flexDirection: 'row',
|
|
295
314
|
alignItems: 'center',
|
|
296
|
-
justifyContent: '
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
logo: {
|
|
302
|
-
fontSize: 14,
|
|
303
|
-
fontWeight: '700',
|
|
304
|
-
color: t.accent,
|
|
305
|
-
letterSpacing: 1.2,
|
|
306
|
-
textTransform: 'uppercase',
|
|
307
|
-
},
|
|
308
|
-
closeBtn: {
|
|
309
|
-
padding: 4,
|
|
315
|
+
justifyContent: 'center',
|
|
316
|
+
paddingVertical: 10,
|
|
317
|
+
paddingBottom: Platform.OS === 'ios' ? 4 : 10,
|
|
318
|
+
borderTopWidth: StyleSheet.hairlineWidth,
|
|
319
|
+
borderTopColor: t.border,
|
|
310
320
|
},
|
|
311
|
-
|
|
312
|
-
fontSize:
|
|
321
|
+
footerText: {
|
|
322
|
+
fontSize: 11,
|
|
313
323
|
color: t.subtext,
|
|
314
324
|
},
|
|
315
|
-
progress: {
|
|
316
|
-
flexDirection: 'row',
|
|
317
|
-
gap: 6,
|
|
318
|
-
paddingHorizontal: 20,
|
|
319
|
-
paddingBottom: 8,
|
|
320
|
-
},
|
|
321
|
-
progressDot: {
|
|
322
|
-
flex: 1,
|
|
323
|
-
height: 3,
|
|
324
|
-
borderRadius: 999,
|
|
325
|
-
backgroundColor: t.border,
|
|
326
|
-
},
|
|
327
|
-
scroll: { flex: 1 },
|
|
328
|
-
scrollContent: { flexGrow: 1 },
|
|
329
325
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @kernaq/verify-react-native
|
|
2
|
+
* @kernaq/verify-react-native v2
|
|
3
3
|
*
|
|
4
4
|
* Drop-in KYC verification UI for React Native.
|
|
5
|
-
*
|
|
5
|
+
* Matches @kernaq/verify web v2.0.0 — same flow, same API surface.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* import { KernaqVerify } from '@kernaq/verify-react-native'
|
|
9
9
|
*
|
|
10
10
|
* <KernaqVerify
|
|
11
11
|
* visible={showVerify}
|
|
12
|
-
*
|
|
12
|
+
* documentType="national_id"
|
|
13
13
|
* country="KEN"
|
|
14
|
+
* reference={userId}
|
|
15
|
+
* steps={['document', 'selfie', 'liveness']}
|
|
14
16
|
* onComplete={(result) => console.log(result.verdict)}
|
|
15
|
-
* onCancel={() => setShowVerify(false)}
|
|
16
17
|
* />
|
|
17
18
|
*
|
|
18
19
|
* Peer dependencies:
|
|
@@ -21,12 +22,16 @@
|
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
export { KernaqVerify } from './KernaqVerify'
|
|
25
|
+
export { analyseImageUri, analyseSelfieFrame } from './quality'
|
|
26
|
+
export type { QualityReport, QualityConfig, QualityFailure } from './quality'
|
|
24
27
|
export type {
|
|
25
28
|
VerifyConfig,
|
|
26
29
|
VerifyResult,
|
|
27
30
|
VerifyStep,
|
|
31
|
+
VerifyStepName,
|
|
28
32
|
VerifyVerdict,
|
|
29
33
|
VerifyTheme,
|
|
30
34
|
VerifyLocale,
|
|
31
35
|
DocumentType,
|
|
36
|
+
LivenessTask,
|
|
32
37
|
} from './types'
|
package/src/locale.ts
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
import type { VerifyLocale } from './types'
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_LOCALE: VerifyLocale = {
|
|
4
|
-
intro_title:
|
|
5
|
-
intro_body:
|
|
6
|
-
intro_cta:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
4
|
+
intro_title: 'Verify your identity',
|
|
5
|
+
intro_body: 'This takes about 90 seconds. You\'ll need your ID document and a clear view of your face.',
|
|
6
|
+
intro_cta: 'Get started',
|
|
7
|
+
|
|
8
|
+
doc_front_title: 'Document — front',
|
|
9
|
+
doc_front_instruction: 'Hold the front of your document flat inside the frame. Ensure all four corners are visible and there is no glare.',
|
|
10
|
+
doc_back_title: 'Document — back',
|
|
11
|
+
doc_back_instruction: 'Flip your document over and hold the back inside the frame.',
|
|
12
|
+
doc_capture_btn: 'Capture',
|
|
13
|
+
doc_retake_btn: 'Retake',
|
|
14
|
+
|
|
15
|
+
selfie_title: 'Selfie',
|
|
16
|
+
selfie_instruction: 'Centre your face inside the oval. Look directly at the camera.',
|
|
17
|
+
|
|
18
|
+
liveness_title: 'Liveness check',
|
|
19
|
+
liveness_ready: 'Follow the instructions as they appear.',
|
|
20
|
+
liveness_task_prefix: 'Please',
|
|
21
|
+
liveness_complete: 'Hold still…',
|
|
22
|
+
|
|
23
|
+
processing_title: 'Verifying…',
|
|
24
|
+
|
|
25
|
+
result_pass_title: 'Identity verified',
|
|
26
|
+
result_pass_body: 'Your verification was successful.',
|
|
27
|
+
result_fail_title: 'Verification failed',
|
|
28
|
+
result_fail_body: 'We could not verify your identity. Please try again with better lighting.',
|
|
29
|
+
result_review_title: 'Under review',
|
|
30
|
+
result_review_body: 'Your submission is being manually reviewed. This usually takes a few minutes.',
|
|
31
|
+
|
|
32
|
+
error_camera_denied: 'Camera access denied. Allow camera permissions and try again.',
|
|
33
|
+
error_blur: 'Image is too blurry — hold steady and ensure good lighting.',
|
|
34
|
+
error_quality: 'Image quality too low. Try better lighting or a steadier hand.',
|
|
35
|
+
error_network: 'Network error. Check your connection and try again.',
|
|
33
36
|
}
|