@kernaq/verify-react-native 2.0.1 → 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.esm.js +63 -22
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +63 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -9
- package/src/KernaqVerify.tsx +43 -16
- package/src/steps/LivenessStep.tsx +15 -8
package/src/KernaqVerify.tsx
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* onComplete={(r) => console.log(r.verdict)}
|
|
22
22
|
* />
|
|
23
23
|
*/
|
|
24
|
-
import React, { useState, useCallback, useMemo } from 'react'
|
|
24
|
+
import React, { useState, useCallback, useMemo, useRef } from 'react'
|
|
25
25
|
import {
|
|
26
26
|
Modal,
|
|
27
27
|
View,
|
|
@@ -75,6 +75,17 @@ export function KernaqVerify({
|
|
|
75
75
|
const [result, setResult] = useState<VerifyResult | null>(null)
|
|
76
76
|
const [error, setError] = useState<string | null>(null)
|
|
77
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
|
+
}
|
|
88
|
+
|
|
78
89
|
const theme = useMemo(() => resolveTheme(themeProp), [themeProp])
|
|
79
90
|
const locale: VerifyLocale = useMemo(() => ({ ...DEFAULT_LOCALE, ...localeProp }), [localeProp])
|
|
80
91
|
const { requestPermissions } = usePermissions()
|
|
@@ -105,30 +116,43 @@ export function KernaqVerify({
|
|
|
105
116
|
}
|
|
106
117
|
setStep(s)
|
|
107
118
|
if (s === 'processing') submit()
|
|
119
|
+
// submit is stable (no deps that change); locale/requestPermissions are stable refs
|
|
108
120
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
-
}, [
|
|
121
|
+
}, [locale])
|
|
110
122
|
|
|
111
123
|
const reset = useCallback(() => {
|
|
112
124
|
setStep('intro')
|
|
125
|
+
captureRef.current = {}
|
|
113
126
|
setCapture({})
|
|
114
127
|
setResult(null)
|
|
115
128
|
setError(null)
|
|
116
129
|
}, [])
|
|
117
130
|
|
|
118
131
|
const submit = useCallback(async () => {
|
|
119
|
-
const { docFrontUri, docBackUri, selfieUri, videoUri } =
|
|
120
|
-
|
|
121
|
-
|
|
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.')
|
|
142
|
+
setStep('result')
|
|
143
|
+
onError?.({ code: 'capture_missing', message: 'Selfie capture missing' })
|
|
144
|
+
return
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
const base = (backendUrl ?? 'https://api.identity.kernaq.com/v1').replace(/\/$/, '')
|
|
148
|
+
const endpoint = sandbox ? '/verify/sandbox' : '/verify'
|
|
125
149
|
const form = new FormData()
|
|
126
150
|
|
|
127
151
|
if (docFrontUri) {
|
|
128
|
-
form.append('
|
|
152
|
+
form.append('doc_front', { uri: docFrontUri, name: 'doc_front.jpg', type: 'image/jpeg' } as any)
|
|
129
153
|
}
|
|
130
154
|
if (docBackUri) {
|
|
131
|
-
form.append('
|
|
155
|
+
form.append('doc_back', { uri: docBackUri, name: 'doc_back.jpg', type: 'image/jpeg' } as any)
|
|
132
156
|
}
|
|
133
157
|
if (selfieUri) {
|
|
134
158
|
form.append('selfie', { uri: selfieUri, name: 'selfie.jpg', type: 'image/jpeg' } as any)
|
|
@@ -140,13 +164,16 @@ export function KernaqVerify({
|
|
|
140
164
|
form.append('document_type', documentType)
|
|
141
165
|
form.append('country', country)
|
|
142
166
|
form.append('reference', reference ?? `kq_${Date.now()}`)
|
|
143
|
-
|
|
167
|
+
// sandbox is handled via URL routing above — no form field needed
|
|
144
168
|
|
|
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 = await fetch(`${base}
|
|
176
|
+
const res = await fetch(`${base}${endpoint}`, { method: 'POST', headers, body: form })
|
|
150
177
|
const body = (await res.json()) as Record<string, unknown>
|
|
151
178
|
if (!res.ok) throw new Error((body['message'] as string) ?? `HTTP ${res.status}`)
|
|
152
179
|
|
|
@@ -170,7 +197,7 @@ export function KernaqVerify({
|
|
|
170
197
|
onError?.({ code: 'network_error', message: msg })
|
|
171
198
|
}
|
|
172
199
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
173
|
-
}, [
|
|
200
|
+
}, []) // no deps — always reads from captureRef.current, never from stale closure
|
|
174
201
|
|
|
175
202
|
return (
|
|
176
203
|
<Modal
|
|
@@ -200,7 +227,7 @@ export function KernaqVerify({
|
|
|
200
227
|
locale={locale}
|
|
201
228
|
side="front"
|
|
202
229
|
onCapture={(uri) => {
|
|
203
|
-
|
|
230
|
+
setCaptureSync(c => ({ ...c, docFrontUri: uri }))
|
|
204
231
|
next('doc-front')
|
|
205
232
|
}}
|
|
206
233
|
/>
|
|
@@ -213,11 +240,11 @@ export function KernaqVerify({
|
|
|
213
240
|
side="back"
|
|
214
241
|
frontUri={capture.docFrontUri}
|
|
215
242
|
onCapture={(uri) => {
|
|
216
|
-
|
|
243
|
+
setCaptureSync(c => ({ ...c, docBackUri: uri }))
|
|
217
244
|
next('doc-back')
|
|
218
245
|
}}
|
|
219
246
|
onRetakeFront={() => {
|
|
220
|
-
|
|
247
|
+
setCaptureSync(c => ({ ...c, docFrontUri: undefined }))
|
|
221
248
|
goto('doc-front')
|
|
222
249
|
}}
|
|
223
250
|
/>
|
|
@@ -228,7 +255,7 @@ export function KernaqVerify({
|
|
|
228
255
|
theme={theme}
|
|
229
256
|
locale={locale}
|
|
230
257
|
onCapture={(uri) => {
|
|
231
|
-
|
|
258
|
+
setCaptureSync(c => ({ ...c, selfieUri: uri }))
|
|
232
259
|
next('selfie')
|
|
233
260
|
}}
|
|
234
261
|
/>
|
|
@@ -241,7 +268,7 @@ export function KernaqVerify({
|
|
|
241
268
|
duration={livenessDuration}
|
|
242
269
|
taskCount={livenessTaskCount}
|
|
243
270
|
onCapture={(uri) => {
|
|
244
|
-
|
|
271
|
+
setCaptureSync(c => ({ ...c, videoUri: uri }))
|
|
245
272
|
next('liveness')
|
|
246
273
|
}}
|
|
247
274
|
/>
|
|
@@ -61,6 +61,14 @@ export function LivenessStep({ theme, locale, duration, taskCount = 2, onCapture
|
|
|
61
61
|
const [complete, setComplete] = useState(false)
|
|
62
62
|
const [recording, setRecording] = useState(false)
|
|
63
63
|
|
|
64
|
+
// Keep a ref in sync with taskIdx so setTimeout/setInterval closures always
|
|
65
|
+
// read the latest value and don't stall on a stale snapshot.
|
|
66
|
+
const taskIdxRef = useRef(0)
|
|
67
|
+
const setTaskIdxSynced = (n: number) => {
|
|
68
|
+
taskIdxRef.current = n
|
|
69
|
+
setTaskIdx(n)
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
const progressTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
65
73
|
const taskTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
66
74
|
const pulseAnim = useRef(new Animated.Value(1)).current
|
|
@@ -82,15 +90,14 @@ export function LivenessStep({ theme, locale, duration, taskCount = 2, onCapture
|
|
|
82
90
|
|
|
83
91
|
const startTask = () => {
|
|
84
92
|
if (!recording) {
|
|
85
|
-
// Begin recording when first task starts
|
|
86
93
|
startRecording(
|
|
87
|
-
(video) => { onCapture(video.uri) },
|
|
94
|
+
(video: import('../hooks/useCamera').VideoResult) => { onCapture(video.uri) },
|
|
88
95
|
() => {}
|
|
89
96
|
)
|
|
90
97
|
setRecording(true)
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
const task = tasks[
|
|
100
|
+
const task = tasks[taskIdxRef.current]
|
|
94
101
|
if (!task) return
|
|
95
102
|
setStarted(true)
|
|
96
103
|
setProgress(0)
|
|
@@ -109,16 +116,16 @@ export function LivenessStep({ theme, locale, duration, taskCount = 2, onCapture
|
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
const nextTask = () => {
|
|
112
|
-
const next =
|
|
113
|
-
setTaskDone(prev => prev + 1)
|
|
119
|
+
const next = taskIdxRef.current + 1
|
|
120
|
+
setTaskDone((prev: number) => prev + 1)
|
|
114
121
|
if (next >= tasks.length) {
|
|
115
122
|
finishLiveness()
|
|
116
123
|
return
|
|
117
124
|
}
|
|
118
|
-
|
|
125
|
+
setTaskIdxSynced(next)
|
|
119
126
|
setStarted(false)
|
|
120
127
|
setProgress(0)
|
|
121
|
-
//
|
|
128
|
+
// Use the ref-based startTask via the timer so it reads the fresh taskIdx
|
|
122
129
|
taskTimer.current = setTimeout(() => startTask(), 500)
|
|
123
130
|
}
|
|
124
131
|
|
|
@@ -151,7 +158,7 @@ export function LivenessStep({ theme, locale, duration, taskCount = 2, onCapture
|
|
|
151
158
|
{/* Task progress dots */}
|
|
152
159
|
{!complete && (
|
|
153
160
|
<View style={s.taskDots}>
|
|
154
|
-
{tasks.map((_, i) => (
|
|
161
|
+
{tasks.map((_: TaskDef, i: number) => (
|
|
155
162
|
<View
|
|
156
163
|
key={i}
|
|
157
164
|
style={[
|