@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.
@@ -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
- }, [capture, locale])
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 } = capture
120
- if (!docFrontUri && !selfieUri) {
121
- // At least one capture needed depending on steps
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('document', { uri: docFrontUri, name: 'doc_front.jpg', type: 'image/jpeg' } as any)
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('document_back', { uri: docBackUri, name: 'doc_back.jpg', type: 'image/jpeg' } as any)
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
- if (sandbox) form.append('sandbox', 'true')
167
+ // sandbox is handled via URL routing above — no form field needed
144
168
 
145
- const headers: Record<string, string> = { 'Content-Type': 'multipart/form-data' }
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}/verify`, { method: 'POST', headers, body: form })
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
- }, [capture])
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
- setCapture(c => ({ ...c, docFrontUri: uri }))
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
- setCapture(c => ({ ...c, docBackUri: uri }))
243
+ setCaptureSync(c => ({ ...c, docBackUri: uri }))
217
244
  next('doc-back')
218
245
  }}
219
246
  onRetakeFront={() => {
220
- setCapture(c => ({ ...c, docFrontUri: undefined }))
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
- setCapture(c => ({ ...c, selfieUri: uri }))
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
- setCapture(c => ({ ...c, videoUri: uri }))
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[taskIdx]
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 = taskIdx + 1
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
- setTaskIdx(next)
125
+ setTaskIdxSynced(next)
119
126
  setStarted(false)
120
127
  setProgress(0)
121
- // Short pause then auto-start next task
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={[