@kernaq/verify-react-native 2.0.0 → 2.0.1
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.d.ts +95 -50
- package/dist/index.esm.js +1121 -571
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1119 -567
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/KernaqVerify.tsx +144 -175
- 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 +218 -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/dist/index.js
CHANGED
|
@@ -7,46 +7,47 @@ var reactNativeVisionCamera = require('react-native-vision-camera');
|
|
|
7
7
|
|
|
8
8
|
const DEFAULT_LOCALE = {
|
|
9
9
|
intro_title: 'Verify your identity',
|
|
10
|
-
intro_body:
|
|
10
|
+
intro_body: 'This takes about 90 seconds. You\'ll need your ID document and a clear view of your face.',
|
|
11
11
|
intro_cta: 'Get started',
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
doc_front_title: 'Document — front',
|
|
13
|
+
doc_front_instruction: 'Hold the front of your document flat inside the frame. Ensure all four corners are visible and there is no glare.',
|
|
14
|
+
doc_back_title: 'Document — back',
|
|
15
|
+
doc_back_instruction: 'Flip your document over and hold the back inside the frame.',
|
|
14
16
|
doc_capture_btn: 'Capture',
|
|
15
17
|
doc_retake_btn: 'Retake',
|
|
16
|
-
|
|
17
|
-
|
|
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',
|
|
18
|
+
selfie_title: 'Selfie',
|
|
19
|
+
selfie_instruction: 'Centre your face inside the oval. Look directly at the camera.',
|
|
22
20
|
liveness_title: 'Liveness check',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
processing_title: 'Verifying
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
result_pass_body: 'Your identity has been verified successfully.',
|
|
21
|
+
liveness_ready: 'Follow the instructions as they appear.',
|
|
22
|
+
liveness_task_prefix: 'Please',
|
|
23
|
+
liveness_complete: 'Hold still…',
|
|
24
|
+
processing_title: 'Verifying…',
|
|
25
|
+
result_pass_title: 'Identity verified',
|
|
26
|
+
result_pass_body: 'Your verification was successful.',
|
|
30
27
|
result_fail_title: 'Verification failed',
|
|
31
|
-
result_fail_body: 'We
|
|
28
|
+
result_fail_body: 'We could not verify your identity. Please try again with better lighting.',
|
|
32
29
|
result_review_title: 'Under review',
|
|
33
|
-
result_review_body: 'Your submission is being reviewed. This usually takes a few minutes.',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
error_quality: 'Image quality too low. Try better lighting or
|
|
37
|
-
error_network: 'Network error.
|
|
30
|
+
result_review_body: 'Your submission is being manually reviewed. This usually takes a few minutes.',
|
|
31
|
+
error_camera_denied: 'Camera access denied. Allow camera permissions and try again.',
|
|
32
|
+
error_blur: 'Image is too blurry — hold steady and ensure good lighting.',
|
|
33
|
+
error_quality: 'Image quality too low. Try better lighting or a steadier hand.',
|
|
34
|
+
error_network: 'Network error. Check your connection and try again.'
|
|
38
35
|
};
|
|
39
36
|
|
|
40
37
|
function resolveTheme(theme) {
|
|
41
38
|
const dark = theme?.mode === 'dark';
|
|
42
39
|
return {
|
|
43
|
-
accent: theme?.accentColor ?? '#111827',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
accent: theme?.accentColor ?? (dark ? '#e5e7eb' : '#111827'),
|
|
41
|
+
accentInv: dark ? '#111827' : '#ffffff',
|
|
42
|
+
bg: theme?.backgroundColor ?? (dark ? '#0f0f0f' : '#ffffff'),
|
|
43
|
+
surface: theme?.cardColor ?? (dark ? '#1a1a1a' : '#f7f7f7'),
|
|
44
|
+
surface2: dark ? '#242424' : '#efefef',
|
|
45
|
+
text: theme?.textColor ?? (dark ? '#f5f5f5' : '#0f0f0f'),
|
|
46
|
+
subtext: theme?.subtextColor ?? (dark ? '#888888' : '#6b7280'),
|
|
47
|
+
border: theme?.borderColor ?? (dark ? '#2a2a2a' : '#e5e5e5'),
|
|
48
|
+
errorBg: dark ? 'rgba(239,68,68,0.1)' : '#fef2f2',
|
|
49
|
+
errorText: dark ? '#f87171' : '#b91c1c',
|
|
50
|
+
radius: theme?.borderRadius ?? 14
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -105,93 +106,90 @@ function IntroStep({ theme, locale, onStart }) {
|
|
|
105
106
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
106
107
|
style: s.container
|
|
107
108
|
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
108
|
-
style: s.
|
|
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, {
|
|
109
|
+
style: s.textBlock
|
|
110
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
132
111
|
style: s.title
|
|
133
112
|
}, locale.intro_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
134
|
-
style: s.
|
|
135
|
-
}, locale.intro_body), /*#__PURE__*/ React.createElement(reactNative.
|
|
113
|
+
style: s.subtitle
|
|
114
|
+
}, locale.intro_body)), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
115
|
+
style: s.hint
|
|
116
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
117
|
+
style: s.hintIcon
|
|
118
|
+
}, "🔒"), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
119
|
+
style: s.hintText
|
|
120
|
+
}, "Your data is discarded immediately after processing. Nothing is stored.")), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
|
|
136
121
|
style: s.btn,
|
|
137
122
|
onPress: onStart,
|
|
138
123
|
activeOpacity: 0.85
|
|
139
124
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
140
125
|
style: s.btnText
|
|
141
|
-
}, locale.intro_cta)
|
|
126
|
+
}, locale.intro_cta), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
127
|
+
style: s.btnArrow
|
|
128
|
+
}, " →")));
|
|
142
129
|
}
|
|
143
130
|
const styles$6 = (t)=>reactNative.StyleSheet.create({
|
|
144
131
|
container: {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
steps: {
|
|
148
|
-
flexDirection: 'row',
|
|
132
|
+
flex: 1,
|
|
133
|
+
padding: 28,
|
|
149
134
|
justifyContent: 'center',
|
|
150
|
-
gap:
|
|
151
|
-
marginBottom: 28
|
|
152
|
-
},
|
|
153
|
-
step: {
|
|
154
|
-
alignItems: 'center',
|
|
155
|
-
gap: 8
|
|
135
|
+
gap: 16
|
|
156
136
|
},
|
|
157
|
-
|
|
158
|
-
width: 56,
|
|
159
|
-
height: 56,
|
|
160
|
-
borderRadius: 14,
|
|
161
|
-
backgroundColor: t.card,
|
|
162
|
-
borderWidth: 1,
|
|
163
|
-
borderColor: t.border,
|
|
137
|
+
textBlock: {
|
|
164
138
|
alignItems: 'center',
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
stepEmoji: {
|
|
168
|
-
fontSize: 24
|
|
169
|
-
},
|
|
170
|
-
stepLabel: {
|
|
171
|
-
fontSize: 12,
|
|
172
|
-
color: t.subtext
|
|
139
|
+
gap: 8,
|
|
140
|
+
marginBottom: 4
|
|
173
141
|
},
|
|
174
142
|
title: {
|
|
175
143
|
fontSize: 22,
|
|
176
144
|
fontWeight: '700',
|
|
177
145
|
color: t.text,
|
|
178
|
-
|
|
146
|
+
textAlign: 'center',
|
|
147
|
+
letterSpacing: -0.4
|
|
179
148
|
},
|
|
180
|
-
|
|
149
|
+
subtitle: {
|
|
181
150
|
fontSize: 14,
|
|
182
151
|
color: t.subtext,
|
|
183
152
|
lineHeight: 21,
|
|
184
|
-
|
|
153
|
+
textAlign: 'center'
|
|
154
|
+
},
|
|
155
|
+
hint: {
|
|
156
|
+
flexDirection: 'row',
|
|
157
|
+
alignItems: 'flex-start',
|
|
158
|
+
gap: 8,
|
|
159
|
+
padding: 12,
|
|
160
|
+
backgroundColor: t.surface,
|
|
161
|
+
borderRadius: t.radius - 4,
|
|
162
|
+
borderWidth: 1,
|
|
163
|
+
borderColor: t.border
|
|
164
|
+
},
|
|
165
|
+
hintIcon: {
|
|
166
|
+
fontSize: 12,
|
|
167
|
+
marginTop: 1
|
|
168
|
+
},
|
|
169
|
+
hintText: {
|
|
170
|
+
flex: 1,
|
|
171
|
+
fontSize: 12,
|
|
172
|
+
color: t.subtext,
|
|
173
|
+
lineHeight: 17
|
|
185
174
|
},
|
|
186
175
|
btn: {
|
|
187
176
|
backgroundColor: t.accent,
|
|
188
|
-
borderRadius: t.radius,
|
|
189
|
-
paddingVertical:
|
|
190
|
-
|
|
177
|
+
borderRadius: t.radius - 4,
|
|
178
|
+
paddingVertical: 13,
|
|
179
|
+
paddingHorizontal: 20,
|
|
180
|
+
flexDirection: 'row',
|
|
181
|
+
alignItems: 'center',
|
|
182
|
+
justifyContent: 'center'
|
|
191
183
|
},
|
|
192
184
|
btnText: {
|
|
193
|
-
color:
|
|
194
|
-
fontSize:
|
|
185
|
+
color: t.accentInv,
|
|
186
|
+
fontSize: 14,
|
|
187
|
+
fontWeight: '600',
|
|
188
|
+
letterSpacing: -0.2
|
|
189
|
+
},
|
|
190
|
+
btnArrow: {
|
|
191
|
+
color: t.accentInv,
|
|
192
|
+
fontSize: 14,
|
|
195
193
|
fontWeight: '600'
|
|
196
194
|
}
|
|
197
195
|
});
|
|
@@ -243,52 +241,256 @@ function useCamera(facing = 'back') {
|
|
|
243
241
|
};
|
|
244
242
|
}
|
|
245
243
|
|
|
246
|
-
|
|
244
|
+
/**
|
|
245
|
+
* quality.ts — Native port of @kernaq/capture-web analyseImage logic.
|
|
246
|
+
*
|
|
247
|
+
* React Native has no OffscreenCanvas or createImageBitmap, so we decode
|
|
248
|
+
* the JPEG manually from its base64 representation to extract a luminance
|
|
249
|
+
* sample, then run the same Laplacian variance + brightness + fill-ratio
|
|
250
|
+
* checks the web SDK uses.
|
|
251
|
+
*
|
|
252
|
+
* This is intentionally dependency-free — no expo-image-manipulator,
|
|
253
|
+
* no worklets, no native modules beyond what vision-camera already provides.
|
|
254
|
+
*
|
|
255
|
+
* The JPEG sampling approach:
|
|
256
|
+
* 1. takeSnapshot({ quality: 20 }) → tiny file (~5-15KB)
|
|
257
|
+
* 2. fetch as ArrayBuffer → raw bytes
|
|
258
|
+
* 3. Skip JPEG header bytes, sample raw DCT-domain luminance bytes
|
|
259
|
+
* as a brightness proxy (not exact, but sufficient for dark/overexposed detection)
|
|
260
|
+
* 4. For blur: compute variance of luminance differences between adjacent samples
|
|
261
|
+
*
|
|
262
|
+
* Limitations vs web SDK:
|
|
263
|
+
* - Fill ratio check is approximated via luminance variance spread
|
|
264
|
+
* - Glasses glare heuristic is the same eye-band brightness check
|
|
265
|
+
* - Blur detection is less precise but calibrated to the same thresholds
|
|
266
|
+
*/ const DEFAULTS = {
|
|
267
|
+
minBlurScore: 25,
|
|
268
|
+
minBrightness: 40,
|
|
269
|
+
maxBrightness: 220,
|
|
270
|
+
minFillRatio: 0.08
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Analyse an image from a file URI.
|
|
274
|
+
* Reads the raw bytes, samples luminance, returns a quality report.
|
|
275
|
+
*/ async function analyseImageUri(uri, config = {}) {
|
|
276
|
+
const cfg = {
|
|
277
|
+
...DEFAULTS,
|
|
278
|
+
...config
|
|
279
|
+
};
|
|
280
|
+
try {
|
|
281
|
+
// Fetch the image as raw bytes
|
|
282
|
+
const resp = await fetch(uri);
|
|
283
|
+
const buffer = await resp.arrayBuffer();
|
|
284
|
+
const bytes = new Uint8Array(buffer);
|
|
285
|
+
// Sample luminance values from the raw bytes
|
|
286
|
+
// JPEG compressed bytes in the entropy-coded segment correlate with luminance
|
|
287
|
+
// We skip the JPEG header (first ~400-600 bytes typically) and sample every Nth byte
|
|
288
|
+
const start = Math.min(600, Math.floor(bytes.length * 0.1));
|
|
289
|
+
const end = Math.floor(bytes.length * 0.9);
|
|
290
|
+
const step = Math.max(1, Math.floor((end - start) / 2048));
|
|
291
|
+
const samples = [];
|
|
292
|
+
for(let i = start; i < end; i += step){
|
|
293
|
+
samples.push(bytes[i]);
|
|
294
|
+
}
|
|
295
|
+
if (samples.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
passed: false,
|
|
298
|
+
blurScore: 0,
|
|
299
|
+
brightness: 0,
|
|
300
|
+
fillRatio: 0,
|
|
301
|
+
failures: [
|
|
302
|
+
'too_blurry'
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const brightness = computeMean(samples);
|
|
307
|
+
const blurScore = computeBlurScore(samples);
|
|
308
|
+
const fillRatio = computeFillProxy(samples);
|
|
309
|
+
const failures = [];
|
|
310
|
+
const cfg2 = cfg;
|
|
311
|
+
if (blurScore < cfg2.minBlurScore) failures.push('too_blurry');
|
|
312
|
+
if (brightness < cfg2.minBrightness) failures.push('too_dark');
|
|
313
|
+
if (brightness > cfg2.maxBrightness) failures.push('too_bright');
|
|
314
|
+
if (fillRatio < cfg2.minFillRatio) failures.push('too_small');
|
|
315
|
+
return {
|
|
316
|
+
passed: failures.length === 0,
|
|
317
|
+
blurScore,
|
|
318
|
+
brightness,
|
|
319
|
+
fillRatio,
|
|
320
|
+
failures
|
|
321
|
+
};
|
|
322
|
+
} catch {
|
|
323
|
+
// If we can't read the file, assume it's a transient error — don't block
|
|
324
|
+
return {
|
|
325
|
+
passed: true,
|
|
326
|
+
blurScore: 50,
|
|
327
|
+
brightness: 128,
|
|
328
|
+
fillRatio: 0.5,
|
|
329
|
+
failures: []
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
334
|
+
function computeMean(samples) {
|
|
335
|
+
return samples.reduce((a, b)=>a + b, 0) / samples.length;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Laplacian variance proxy: variance of first-difference between adjacent samples.
|
|
339
|
+
* Normalised to 0–100. Higher = more high-frequency content = sharper.
|
|
340
|
+
*/ function computeBlurScore(samples) {
|
|
341
|
+
if (samples.length < 2) return 0;
|
|
342
|
+
let sumSq = 0;
|
|
343
|
+
let sum = 0;
|
|
344
|
+
for(let i = 1; i < samples.length; i++){
|
|
345
|
+
const d = samples[i] - samples[i - 1];
|
|
346
|
+
sum += d;
|
|
347
|
+
sumSq += d * d;
|
|
348
|
+
}
|
|
349
|
+
const n = samples.length - 1;
|
|
350
|
+
const mean = sum / n;
|
|
351
|
+
const variance = sumSq / n - mean * mean;
|
|
352
|
+
// Empirically ~0 = solid colour (blurry), ~800+ = sharp photo
|
|
353
|
+
return Math.min(100, Math.round(variance / 800 * 100));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Fill ratio proxy: fraction of samples that differ significantly from the mean.
|
|
357
|
+
* Analogous to the foreground pixel fraction in the web SDK.
|
|
358
|
+
*/ function computeFillProxy(samples) {
|
|
359
|
+
const mean = computeMean(samples);
|
|
360
|
+
const threshold = 30;
|
|
361
|
+
const foreground = samples.filter((s)=>Math.abs(s - mean) > threshold).length;
|
|
362
|
+
return foreground / samples.length;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Selfie-specific quality check with glasses/glare heuristic.
|
|
366
|
+
* Takes snapshots from two regions: full frame and top-40% (eye area).
|
|
367
|
+
*
|
|
368
|
+
* Returns a guidance message (empty string = all good) and a passing flag.
|
|
369
|
+
*/ async function analyseSelfieFrame(uri, config = {}) {
|
|
370
|
+
const cfg = {
|
|
371
|
+
...DEFAULTS,
|
|
372
|
+
...config
|
|
373
|
+
};
|
|
374
|
+
try {
|
|
375
|
+
const resp = await fetch(uri);
|
|
376
|
+
const buffer = await resp.arrayBuffer();
|
|
377
|
+
const bytes = new Uint8Array(buffer);
|
|
378
|
+
const total = bytes.length;
|
|
379
|
+
const start = Math.min(600, Math.floor(total * 0.1));
|
|
380
|
+
const step = Math.max(1, Math.floor((total - start) / 1024));
|
|
381
|
+
// Full-frame samples
|
|
382
|
+
const full = [];
|
|
383
|
+
for(let i = start; i < total; i += step)full.push(bytes[i]);
|
|
384
|
+
// Top-40% samples (eye region proxy — first 40% of the data after header)
|
|
385
|
+
const eyeEnd = start + Math.floor((total - start) * 0.4);
|
|
386
|
+
const eyeSamples = [];
|
|
387
|
+
for(let i = start; i < eyeEnd; i += step)eyeSamples.push(bytes[i]);
|
|
388
|
+
const brightness = computeMean(full);
|
|
389
|
+
const blurScore = computeBlurScore(full);
|
|
390
|
+
const eyeMean = computeMean(eyeSamples);
|
|
391
|
+
// Priority-ordered checks — mirrors web component exactly
|
|
392
|
+
if (brightness < cfg.minBrightness) return {
|
|
393
|
+
passing: false,
|
|
394
|
+
message: 'Move to better lighting'
|
|
395
|
+
};
|
|
396
|
+
if (brightness > cfg.maxBrightness) return {
|
|
397
|
+
passing: false,
|
|
398
|
+
message: 'Too bright — avoid direct light'
|
|
399
|
+
};
|
|
400
|
+
if (eyeMean > 210) return {
|
|
401
|
+
passing: false,
|
|
402
|
+
message: 'Remove glasses'
|
|
403
|
+
};
|
|
404
|
+
if (blurScore < cfg.minBlurScore) return {
|
|
405
|
+
passing: false,
|
|
406
|
+
message: 'Hold still'
|
|
407
|
+
};
|
|
408
|
+
return {
|
|
409
|
+
passing: true,
|
|
410
|
+
message: 'Hold still\u2026'
|
|
411
|
+
};
|
|
412
|
+
} catch {
|
|
413
|
+
return {
|
|
414
|
+
passing: false,
|
|
415
|
+
message: 'Hold still'
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function DocumentStep({ theme, locale, side, frontUri, onCapture, onRetakeFront }) {
|
|
247
421
|
const { cameraRef, device, capturePhoto } = useCamera('back');
|
|
248
|
-
const [preview, setPreview] = React.useState(null);
|
|
249
422
|
const [capturing, setCapturing] = React.useState(false);
|
|
423
|
+
const [error, setError] = React.useState(null);
|
|
250
424
|
const s = styles$5(theme);
|
|
251
425
|
const handleCapture = async ()=>{
|
|
426
|
+
if (!cameraRef.current) return;
|
|
252
427
|
setCapturing(true);
|
|
428
|
+
setError(null);
|
|
253
429
|
const result = await capturePhoto();
|
|
430
|
+
if (!result) {
|
|
431
|
+
setError(locale.error_blur);
|
|
432
|
+
setCapturing(false);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Run same quality checks as web SDK
|
|
436
|
+
const report = await analyseImageUri(result.uri, {
|
|
437
|
+
minBlurScore: 20,
|
|
438
|
+
minBrightness: 30,
|
|
439
|
+
maxBrightness: 230,
|
|
440
|
+
minFillRatio: 0.05
|
|
441
|
+
});
|
|
254
442
|
setCapturing(false);
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
443
|
+
if (!report.passed) {
|
|
444
|
+
const msgs = {
|
|
445
|
+
too_blurry: locale.error_blur,
|
|
446
|
+
too_dark: 'Move to better lighting',
|
|
447
|
+
too_bright: 'Reduce brightness or move away from light',
|
|
448
|
+
too_small: 'Move closer to the document'
|
|
449
|
+
};
|
|
450
|
+
setError(msgs[report.failures[0]] ?? locale.error_quality);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
onCapture(result.uri);
|
|
259
454
|
};
|
|
455
|
+
const title = side === 'front' ? locale.doc_front_title : locale.doc_back_title;
|
|
456
|
+
const instruction = side === 'front' ? locale.doc_front_instruction : locale.doc_back_instruction;
|
|
457
|
+
const hint = side === 'front' ? 'Hold the camera parallel to the document. Move closer if text is hard to read.' : 'Flip the document over and keep it flat inside the frame.';
|
|
260
458
|
if (!device) {
|
|
261
459
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
262
|
-
style:
|
|
460
|
+
style: [
|
|
461
|
+
s.container,
|
|
462
|
+
{
|
|
463
|
+
justifyContent: 'center',
|
|
464
|
+
alignItems: 'center'
|
|
465
|
+
}
|
|
466
|
+
]
|
|
263
467
|
}, /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
|
|
264
468
|
color: theme.accent
|
|
265
469
|
}));
|
|
266
470
|
}
|
|
267
471
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
268
472
|
style: s.container
|
|
473
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
474
|
+
style: s.heading
|
|
269
475
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
270
476
|
style: s.title
|
|
271
|
-
},
|
|
477
|
+
}, title), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
272
478
|
style: s.subtitle
|
|
273
|
-
},
|
|
479
|
+
}, instruction)), error ? /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
480
|
+
style: s.errorBanner
|
|
481
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
482
|
+
style: s.errorText
|
|
483
|
+
}, error)) : null, side === 'back' && frontUri ? /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
484
|
+
style: s.thumbRow
|
|
485
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Image, {
|
|
274
486
|
source: {
|
|
275
|
-
uri:
|
|
487
|
+
uri: frontUri
|
|
276
488
|
},
|
|
277
|
-
style: s.
|
|
489
|
+
style: s.thumb,
|
|
278
490
|
resizeMode: "cover"
|
|
279
|
-
}), /*#__PURE__*/ React.createElement(reactNative.
|
|
280
|
-
style: s.
|
|
281
|
-
|
|
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, {
|
|
491
|
+
}), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
492
|
+
style: s.thumbLabel
|
|
493
|
+
}, "✓ Front captured")) : null, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
292
494
|
style: s.viewport
|
|
293
495
|
}, /*#__PURE__*/ React.createElement(reactNativeVisionCamera.Camera, {
|
|
294
496
|
ref: cameraRef,
|
|
@@ -299,104 +501,287 @@ function DocumentStep({ theme, locale, onCapture }) {
|
|
|
299
501
|
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
300
502
|
style: s.frameGuide,
|
|
301
503
|
pointerEvents: "none"
|
|
302
|
-
}
|
|
504
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
505
|
+
style: [
|
|
506
|
+
s.corner,
|
|
507
|
+
s.tl
|
|
508
|
+
]
|
|
509
|
+
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
303
510
|
style: [
|
|
304
|
-
s.
|
|
511
|
+
s.corner,
|
|
512
|
+
s.tr
|
|
513
|
+
]
|
|
514
|
+
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
515
|
+
style: [
|
|
516
|
+
s.corner,
|
|
517
|
+
s.bl
|
|
518
|
+
]
|
|
519
|
+
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
520
|
+
style: [
|
|
521
|
+
s.corner,
|
|
522
|
+
s.br
|
|
523
|
+
]
|
|
524
|
+
})), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
525
|
+
style: s.dimOverlay,
|
|
526
|
+
pointerEvents: "none"
|
|
527
|
+
})), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
528
|
+
style: s.hint
|
|
529
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
530
|
+
style: s.hintText
|
|
531
|
+
}, hint)), /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
|
|
532
|
+
style: [
|
|
533
|
+
s.btn,
|
|
305
534
|
capturing && s.btnDisabled
|
|
306
535
|
],
|
|
307
536
|
onPress: handleCapture,
|
|
308
537
|
disabled: capturing,
|
|
309
538
|
activeOpacity: 0.85
|
|
310
539
|
}, capturing ? /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
|
|
311
|
-
color:
|
|
540
|
+
color: theme.accentInv
|
|
312
541
|
}) : /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
313
542
|
style: s.btnText
|
|
314
|
-
}, locale.doc_capture_btn))
|
|
543
|
+
}, locale.doc_capture_btn)), side === 'back' && onRetakeFront ? /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
|
|
544
|
+
style: s.btnGhost,
|
|
545
|
+
onPress: onRetakeFront,
|
|
546
|
+
activeOpacity: 0.75
|
|
547
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
548
|
+
style: s.btnGhostText
|
|
549
|
+
}, locale.doc_retake_btn, " front")) : null);
|
|
315
550
|
}
|
|
551
|
+
const CORNER_SIZE = 20;
|
|
552
|
+
const CORNER_W = 2;
|
|
316
553
|
const styles$5 = (t)=>reactNative.StyleSheet.create({
|
|
317
554
|
container: {
|
|
318
|
-
|
|
555
|
+
flex: 1,
|
|
556
|
+
padding: 20,
|
|
557
|
+
gap: 12
|
|
558
|
+
},
|
|
559
|
+
heading: {
|
|
560
|
+
gap: 4,
|
|
561
|
+
alignItems: 'center'
|
|
319
562
|
},
|
|
320
563
|
title: {
|
|
321
564
|
fontSize: 20,
|
|
322
565
|
fontWeight: '700',
|
|
323
566
|
color: t.text,
|
|
324
|
-
|
|
567
|
+
textAlign: 'center',
|
|
568
|
+
letterSpacing: -0.3
|
|
325
569
|
},
|
|
326
570
|
subtitle: {
|
|
327
|
-
fontSize:
|
|
571
|
+
fontSize: 13,
|
|
328
572
|
color: t.subtext,
|
|
329
|
-
lineHeight:
|
|
330
|
-
|
|
573
|
+
lineHeight: 19,
|
|
574
|
+
textAlign: 'center'
|
|
575
|
+
},
|
|
576
|
+
errorBanner: {
|
|
577
|
+
backgroundColor: t.errorBg,
|
|
578
|
+
borderRadius: 8,
|
|
579
|
+
padding: 10
|
|
580
|
+
},
|
|
581
|
+
errorText: {
|
|
582
|
+
fontSize: 12.5,
|
|
583
|
+
color: t.errorText,
|
|
584
|
+
lineHeight: 17
|
|
585
|
+
},
|
|
586
|
+
thumbRow: {
|
|
587
|
+
flexDirection: 'row',
|
|
588
|
+
alignItems: 'center',
|
|
589
|
+
gap: 10,
|
|
590
|
+
padding: 8,
|
|
591
|
+
backgroundColor: t.surface,
|
|
592
|
+
borderRadius: 8,
|
|
593
|
+
borderWidth: 1,
|
|
594
|
+
borderColor: t.border
|
|
595
|
+
},
|
|
596
|
+
thumb: {
|
|
597
|
+
width: 48,
|
|
598
|
+
height: 32,
|
|
599
|
+
borderRadius: 5,
|
|
600
|
+
borderWidth: 1,
|
|
601
|
+
borderColor: t.border
|
|
602
|
+
},
|
|
603
|
+
thumbLabel: {
|
|
604
|
+
fontSize: 12,
|
|
605
|
+
color: t.subtext
|
|
331
606
|
},
|
|
332
607
|
viewport: {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
borderRadius: t.radius,
|
|
608
|
+
flex: 1,
|
|
609
|
+
borderRadius: t.radius - 4,
|
|
336
610
|
overflow: 'hidden',
|
|
337
611
|
backgroundColor: '#000',
|
|
338
|
-
marginBottom: 16,
|
|
339
612
|
position: 'relative'
|
|
340
613
|
},
|
|
341
614
|
frameGuide: {
|
|
342
615
|
position: 'absolute',
|
|
343
616
|
top: '8%',
|
|
344
|
-
left: '
|
|
345
|
-
right: '
|
|
346
|
-
bottom: '8%'
|
|
347
|
-
borderWidth: 2,
|
|
348
|
-
borderColor: 'rgba(255,255,255,0.7)',
|
|
349
|
-
borderRadius: 8
|
|
617
|
+
left: '6%',
|
|
618
|
+
right: '6%',
|
|
619
|
+
bottom: '8%'
|
|
350
620
|
},
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
621
|
+
corner: {
|
|
622
|
+
position: 'absolute',
|
|
623
|
+
width: CORNER_SIZE,
|
|
624
|
+
height: CORNER_SIZE,
|
|
625
|
+
borderColor: 'rgba(255,255,255,0.9)'
|
|
626
|
+
},
|
|
627
|
+
tl: {
|
|
628
|
+
top: 0,
|
|
629
|
+
left: 0,
|
|
630
|
+
borderTopWidth: CORNER_W,
|
|
631
|
+
borderLeftWidth: CORNER_W,
|
|
632
|
+
borderTopLeftRadius: 3
|
|
633
|
+
},
|
|
634
|
+
tr: {
|
|
635
|
+
top: 0,
|
|
636
|
+
right: 0,
|
|
637
|
+
borderTopWidth: CORNER_W,
|
|
638
|
+
borderRightWidth: CORNER_W,
|
|
639
|
+
borderTopRightRadius: 3
|
|
640
|
+
},
|
|
641
|
+
bl: {
|
|
642
|
+
bottom: 0,
|
|
643
|
+
left: 0,
|
|
644
|
+
borderBottomWidth: CORNER_W,
|
|
645
|
+
borderLeftWidth: CORNER_W,
|
|
646
|
+
borderBottomLeftRadius: 3
|
|
647
|
+
},
|
|
648
|
+
br: {
|
|
649
|
+
bottom: 0,
|
|
650
|
+
right: 0,
|
|
651
|
+
borderBottomWidth: CORNER_W,
|
|
652
|
+
borderRightWidth: CORNER_W,
|
|
653
|
+
borderBottomRightRadius: 3
|
|
654
|
+
},
|
|
655
|
+
dimOverlay: {
|
|
656
|
+
position: 'absolute',
|
|
657
|
+
top: 0,
|
|
658
|
+
left: 0,
|
|
659
|
+
right: 0,
|
|
660
|
+
bottom: 0,
|
|
661
|
+
backgroundColor: 'transparent'
|
|
662
|
+
},
|
|
663
|
+
hint: {
|
|
664
|
+
flexDirection: 'row',
|
|
665
|
+
gap: 7,
|
|
666
|
+
padding: 9,
|
|
667
|
+
backgroundColor: t.surface,
|
|
668
|
+
borderRadius: 8,
|
|
356
669
|
borderWidth: 1,
|
|
357
670
|
borderColor: t.border
|
|
358
671
|
},
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
marginBottom: 10
|
|
672
|
+
hintText: {
|
|
673
|
+
flex: 1,
|
|
674
|
+
fontSize: 12,
|
|
675
|
+
color: t.subtext,
|
|
676
|
+
lineHeight: 16
|
|
365
677
|
},
|
|
366
|
-
|
|
367
|
-
backgroundColor: t.
|
|
368
|
-
borderRadius: t.radius,
|
|
369
|
-
paddingVertical:
|
|
370
|
-
alignItems: 'center'
|
|
371
|
-
borderWidth: 1,
|
|
372
|
-
borderColor: t.border
|
|
678
|
+
btn: {
|
|
679
|
+
backgroundColor: t.accent,
|
|
680
|
+
borderRadius: t.radius - 4,
|
|
681
|
+
paddingVertical: 12,
|
|
682
|
+
alignItems: 'center'
|
|
373
683
|
},
|
|
374
684
|
btnDisabled: {
|
|
375
|
-
opacity: 0.
|
|
685
|
+
opacity: 0.5
|
|
376
686
|
},
|
|
377
687
|
btnText: {
|
|
378
|
-
color:
|
|
379
|
-
fontSize:
|
|
688
|
+
color: t.accentInv,
|
|
689
|
+
fontSize: 14,
|
|
380
690
|
fontWeight: '600'
|
|
381
691
|
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
692
|
+
btnGhost: {
|
|
693
|
+
borderRadius: t.radius - 4,
|
|
694
|
+
paddingVertical: 11,
|
|
695
|
+
alignItems: 'center',
|
|
696
|
+
borderWidth: 1,
|
|
697
|
+
borderColor: t.border,
|
|
698
|
+
backgroundColor: 'transparent'
|
|
699
|
+
},
|
|
700
|
+
btnGhostText: {
|
|
701
|
+
color: t.subtext,
|
|
702
|
+
fontSize: 13.5,
|
|
703
|
+
fontWeight: '500'
|
|
386
704
|
}
|
|
387
705
|
});
|
|
388
706
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
707
|
+
const HOLD_FRAMES = 12 // ~1.5s at 120ms per frame
|
|
708
|
+
;
|
|
709
|
+
function SelfieStep({ theme, locale, onCapture }) {
|
|
710
|
+
const cameraRef = React.useRef(null);
|
|
711
|
+
const device = reactNativeVisionCamera.useCameraDevice('front');
|
|
712
|
+
const [pill, setPill] = React.useState('Align your face');
|
|
713
|
+
const [pillOk, setPillOk] = React.useState(false);
|
|
392
714
|
const [capturing, setCapturing] = React.useState(false);
|
|
715
|
+
const readyFrames = React.useRef(0);
|
|
716
|
+
const timerRef = React.useRef(null);
|
|
717
|
+
const borderAnim = React.useRef(new reactNative.Animated.Value(0)).current // 0 = white, 1 = green
|
|
718
|
+
;
|
|
393
719
|
const s = styles$4(theme);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
720
|
+
// Animate oval border colour
|
|
721
|
+
React.useEffect(()=>{
|
|
722
|
+
reactNative.Animated.timing(borderAnim, {
|
|
723
|
+
toValue: pillOk ? 1 : 0,
|
|
724
|
+
duration: 200,
|
|
725
|
+
useNativeDriver: false
|
|
726
|
+
}).start();
|
|
727
|
+
}, [
|
|
728
|
+
pillOk,
|
|
729
|
+
borderAnim
|
|
730
|
+
]);
|
|
731
|
+
const ovalBorder = borderAnim.interpolate({
|
|
732
|
+
inputRange: [
|
|
733
|
+
0,
|
|
734
|
+
1
|
|
735
|
+
],
|
|
736
|
+
outputRange: [
|
|
737
|
+
'rgba(255,255,255,0.75)',
|
|
738
|
+
'#4ade80'
|
|
739
|
+
]
|
|
740
|
+
});
|
|
741
|
+
React.useEffect(()=>{
|
|
742
|
+
if (!device || capturing) return;
|
|
743
|
+
timerRef.current = setInterval(async ()=>{
|
|
744
|
+
if (!cameraRef.current || capturing) return;
|
|
745
|
+
try {
|
|
746
|
+
// Fast low-quality snapshot for analysis only
|
|
747
|
+
const snap = await cameraRef.current.takeSnapshot({
|
|
748
|
+
quality: 20
|
|
749
|
+
});
|
|
750
|
+
const uri = `file://${snap.path}`;
|
|
751
|
+
const { passing, message } = await analyseSelfieFrame(uri, {
|
|
752
|
+
minBlurScore: 22,
|
|
753
|
+
minBrightness: 40,
|
|
754
|
+
maxBrightness: 220
|
|
755
|
+
});
|
|
756
|
+
setPill(passing ? 'Hold still\u2026' : message);
|
|
757
|
+
setPillOk(passing);
|
|
758
|
+
if (passing) {
|
|
759
|
+
readyFrames.current++;
|
|
760
|
+
} else {
|
|
761
|
+
readyFrames.current = 0;
|
|
762
|
+
}
|
|
763
|
+
if (readyFrames.current >= HOLD_FRAMES) {
|
|
764
|
+
// Stop loop, take full-quality photo
|
|
765
|
+
clearInterval(timerRef.current);
|
|
766
|
+
timerRef.current = null;
|
|
767
|
+
setCapturing(true);
|
|
768
|
+
const full = await cameraRef.current.takePhoto({
|
|
769
|
+
qualityPrioritization: 'quality'
|
|
770
|
+
});
|
|
771
|
+
onCapture(`file://${full.path}`);
|
|
772
|
+
}
|
|
773
|
+
} catch {
|
|
774
|
+
readyFrames.current = 0;
|
|
775
|
+
}
|
|
776
|
+
}, 120);
|
|
777
|
+
return ()=>{
|
|
778
|
+
if (timerRef.current) clearInterval(timerRef.current);
|
|
779
|
+
};
|
|
780
|
+
}, [
|
|
781
|
+
device,
|
|
782
|
+
capturing,
|
|
783
|
+
onCapture
|
|
784
|
+
]);
|
|
400
785
|
if (!device) return /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
|
|
401
786
|
color: theme.accent,
|
|
402
787
|
style: {
|
|
@@ -405,148 +790,181 @@ function SelfieStep({ theme, locale, onCapture, onBack }) {
|
|
|
405
790
|
});
|
|
406
791
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
407
792
|
style: s.container
|
|
793
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
794
|
+
style: s.heading
|
|
408
795
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
409
796
|
style: s.title
|
|
410
797
|
}, locale.selfie_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
411
798
|
style: s.subtitle
|
|
412
|
-
}, locale.selfie_instruction),
|
|
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, {
|
|
799
|
+
}, locale.selfie_instruction)), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
431
800
|
style: s.viewport
|
|
432
801
|
}, /*#__PURE__*/ React.createElement(reactNativeVisionCamera.Camera, {
|
|
433
802
|
ref: cameraRef,
|
|
434
803
|
style: reactNative.StyleSheet.absoluteFill,
|
|
435
804
|
device: device,
|
|
436
|
-
isActive:
|
|
805
|
+
isActive: !capturing,
|
|
437
806
|
photo: true
|
|
438
|
-
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
439
|
-
style:
|
|
807
|
+
}), /*#__PURE__*/ React.createElement(reactNative.Animated.View, {
|
|
808
|
+
style: [
|
|
809
|
+
s.oval,
|
|
810
|
+
{
|
|
811
|
+
borderColor: ovalBorder
|
|
812
|
+
}
|
|
813
|
+
],
|
|
440
814
|
pointerEvents: "none"
|
|
441
|
-
})
|
|
815
|
+
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
442
816
|
style: [
|
|
443
|
-
s.
|
|
444
|
-
|
|
817
|
+
s.pill,
|
|
818
|
+
pillOk && s.pillOk
|
|
445
819
|
],
|
|
446
|
-
|
|
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
|
|
820
|
+
pointerEvents: "none"
|
|
457
821
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
458
|
-
style: s.
|
|
459
|
-
},
|
|
822
|
+
style: s.pillText
|
|
823
|
+
}, pill))), capturing && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
824
|
+
style: s.capturingRow
|
|
825
|
+
}, /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
|
|
826
|
+
color: theme.accent,
|
|
827
|
+
size: "small"
|
|
828
|
+
}), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
829
|
+
style: s.capturingText
|
|
830
|
+
}, "Capturing…")));
|
|
460
831
|
}
|
|
461
832
|
const styles$4 = (t)=>reactNative.StyleSheet.create({
|
|
462
833
|
container: {
|
|
463
|
-
|
|
834
|
+
flex: 1,
|
|
835
|
+
padding: 20,
|
|
836
|
+
gap: 12
|
|
837
|
+
},
|
|
838
|
+
heading: {
|
|
839
|
+
gap: 4,
|
|
840
|
+
alignItems: 'center'
|
|
464
841
|
},
|
|
465
842
|
title: {
|
|
466
843
|
fontSize: 20,
|
|
467
844
|
fontWeight: '700',
|
|
468
845
|
color: t.text,
|
|
469
|
-
|
|
846
|
+
textAlign: 'center',
|
|
847
|
+
letterSpacing: -0.3
|
|
470
848
|
},
|
|
471
849
|
subtitle: {
|
|
472
|
-
fontSize:
|
|
850
|
+
fontSize: 13,
|
|
473
851
|
color: t.subtext,
|
|
474
|
-
lineHeight:
|
|
475
|
-
|
|
852
|
+
lineHeight: 19,
|
|
853
|
+
textAlign: 'center'
|
|
476
854
|
},
|
|
477
855
|
viewport: {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
borderRadius: t.radius,
|
|
856
|
+
flex: 1,
|
|
857
|
+
borderRadius: t.radius - 4,
|
|
481
858
|
overflow: 'hidden',
|
|
482
859
|
backgroundColor: '#000',
|
|
483
|
-
|
|
484
|
-
position: 'relative',
|
|
485
|
-
alignSelf: 'center',
|
|
486
|
-
maxWidth: 280
|
|
860
|
+
position: 'relative'
|
|
487
861
|
},
|
|
488
|
-
|
|
862
|
+
oval: {
|
|
489
863
|
position: 'absolute',
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
bottom: '
|
|
864
|
+
left: '21%',
|
|
865
|
+
right: '21%',
|
|
866
|
+
top: '6%',
|
|
867
|
+
bottom: '6%',
|
|
494
868
|
borderWidth: 2,
|
|
495
|
-
borderColor: 'rgba(255,255,255,0.7)',
|
|
496
869
|
borderRadius: 999
|
|
497
870
|
},
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
borderRadius: t.radius,
|
|
502
|
-
marginBottom: 16,
|
|
871
|
+
pill: {
|
|
872
|
+
position: 'absolute',
|
|
873
|
+
bottom: 14,
|
|
503
874
|
alignSelf: 'center',
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
|
875
|
+
backgroundColor: 'rgba(0,0,0,0.65)',
|
|
876
|
+
borderRadius: 999,
|
|
877
|
+
paddingHorizontal: 16,
|
|
878
|
+
paddingVertical: 7,
|
|
879
|
+
borderWidth: 1.5,
|
|
880
|
+
borderColor: 'rgba(255,255,255,0.15)'
|
|
521
881
|
},
|
|
522
|
-
|
|
523
|
-
|
|
882
|
+
pillOk: {
|
|
883
|
+
backgroundColor: 'rgba(22,163,74,0.8)',
|
|
884
|
+
borderColor: 'rgba(134,239,172,0.5)'
|
|
524
885
|
},
|
|
525
|
-
|
|
886
|
+
pillText: {
|
|
526
887
|
color: '#fff',
|
|
527
|
-
fontSize:
|
|
888
|
+
fontSize: 13,
|
|
528
889
|
fontWeight: '600'
|
|
529
890
|
},
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
891
|
+
capturingRow: {
|
|
892
|
+
flexDirection: 'row',
|
|
893
|
+
alignItems: 'center',
|
|
894
|
+
justifyContent: 'center',
|
|
895
|
+
gap: 8,
|
|
896
|
+
paddingVertical: 8
|
|
897
|
+
},
|
|
898
|
+
capturingText: {
|
|
899
|
+
fontSize: 13,
|
|
900
|
+
color: t.subtext
|
|
534
901
|
}
|
|
535
902
|
});
|
|
536
903
|
|
|
537
|
-
|
|
538
|
-
|
|
904
|
+
const TASK_POOL = [
|
|
905
|
+
{
|
|
906
|
+
id: 'blink',
|
|
907
|
+
label: 'Blink twice',
|
|
908
|
+
duration: 3000
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
id: 'turn-left',
|
|
912
|
+
label: 'Turn your head left',
|
|
913
|
+
duration: 3000
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: 'turn-right',
|
|
917
|
+
label: 'Turn your head right',
|
|
918
|
+
duration: 3000
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: 'nod',
|
|
922
|
+
label: 'Nod your head',
|
|
923
|
+
duration: 3000
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
id: 'open-mouth',
|
|
927
|
+
label: 'Open your mouth',
|
|
928
|
+
duration: 3000
|
|
929
|
+
}
|
|
930
|
+
];
|
|
931
|
+
function shuffle(arr) {
|
|
932
|
+
const a = [
|
|
933
|
+
...arr
|
|
934
|
+
];
|
|
935
|
+
for(let i = a.length - 1; i > 0; i--){
|
|
936
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
937
|
+
[a[i], a[j]] = [
|
|
938
|
+
a[j],
|
|
939
|
+
a[i]
|
|
940
|
+
];
|
|
941
|
+
}
|
|
942
|
+
return a;
|
|
943
|
+
}
|
|
944
|
+
function LivenessStep({ theme, locale, duration, taskCount = 2, onCapture }) {
|
|
945
|
+
const { cameraRef, startRecording, stopRecording } = useCamera('front');
|
|
946
|
+
const device = reactNativeVisionCamera.useCameraDevice('front');
|
|
947
|
+
const tasks = React.useMemo(()=>shuffle(TASK_POOL).slice(0, Math.min(taskCount, TASK_POOL.length)), [
|
|
948
|
+
taskCount
|
|
949
|
+
]);
|
|
950
|
+
const [taskIdx, setTaskIdx] = React.useState(0);
|
|
951
|
+
const [started, setStarted] = React.useState(false) // task in progress
|
|
952
|
+
;
|
|
953
|
+
const [taskDone, setTaskDone] = React.useState(0) // completed tasks count
|
|
954
|
+
;
|
|
955
|
+
const [progress, setProgress] = React.useState(0) // 0–100 for current task bar
|
|
956
|
+
;
|
|
957
|
+
const [complete, setComplete] = React.useState(false);
|
|
539
958
|
const [recording, setRecording] = React.useState(false);
|
|
540
|
-
const
|
|
541
|
-
const
|
|
959
|
+
const progressTimer = React.useRef(null);
|
|
960
|
+
const taskTimer = React.useRef(null);
|
|
542
961
|
const pulseAnim = React.useRef(new reactNative.Animated.Value(1)).current;
|
|
543
|
-
|
|
544
|
-
// Pulse animation for recording indicator
|
|
962
|
+
// Pulse REC dot
|
|
545
963
|
React.useEffect(()=>{
|
|
546
964
|
if (recording) {
|
|
547
965
|
reactNative.Animated.loop(reactNative.Animated.sequence([
|
|
548
966
|
reactNative.Animated.timing(pulseAnim, {
|
|
549
|
-
toValue: 0.
|
|
967
|
+
toValue: 0.2,
|
|
550
968
|
duration: 600,
|
|
551
969
|
useNativeDriver: true
|
|
552
970
|
}),
|
|
@@ -557,34 +975,60 @@ function LivenessStep({ theme, locale, duration, onCapture, onBack }) {
|
|
|
557
975
|
})
|
|
558
976
|
])).start();
|
|
559
977
|
} else {
|
|
978
|
+
pulseAnim.stopAnimation();
|
|
560
979
|
pulseAnim.setValue(1);
|
|
561
980
|
}
|
|
562
981
|
}, [
|
|
563
982
|
recording,
|
|
564
983
|
pulseAnim
|
|
565
984
|
]);
|
|
566
|
-
const
|
|
567
|
-
if (!
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
985
|
+
const startTask = ()=>{
|
|
986
|
+
if (!recording) {
|
|
987
|
+
// Begin recording when first task starts
|
|
988
|
+
startRecording((video)=>{
|
|
989
|
+
onCapture(video.uri);
|
|
990
|
+
}, ()=>{});
|
|
991
|
+
setRecording(true);
|
|
992
|
+
}
|
|
993
|
+
const task = tasks[taskIdx];
|
|
994
|
+
if (!task) return;
|
|
995
|
+
setStarted(true);
|
|
996
|
+
setProgress(0);
|
|
997
|
+
const start = Date.now();
|
|
998
|
+
progressTimer.current = setInterval(()=>{
|
|
999
|
+
const elapsed = Date.now() - start;
|
|
1000
|
+
const pct = Math.min(100, elapsed / task.duration * 100);
|
|
1001
|
+
setProgress(pct);
|
|
1002
|
+
if (elapsed >= task.duration) {
|
|
1003
|
+
clearInterval(progressTimer.current);
|
|
1004
|
+
progressTimer.current = null;
|
|
1005
|
+
nextTask();
|
|
582
1006
|
}
|
|
583
|
-
},
|
|
1007
|
+
}, 80);
|
|
1008
|
+
};
|
|
1009
|
+
const nextTask = ()=>{
|
|
1010
|
+
const next = taskIdx + 1;
|
|
1011
|
+
setTaskDone((prev)=>prev + 1);
|
|
1012
|
+
if (next >= tasks.length) {
|
|
1013
|
+
finishLiveness();
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
setTaskIdx(next);
|
|
1017
|
+
setStarted(false);
|
|
1018
|
+
setProgress(0);
|
|
1019
|
+
// Short pause then auto-start next task
|
|
1020
|
+
taskTimer.current = setTimeout(()=>startTask(), 500);
|
|
1021
|
+
};
|
|
1022
|
+
const finishLiveness = ()=>{
|
|
1023
|
+
setComplete(true);
|
|
1024
|
+
taskTimer.current = setTimeout(()=>{
|
|
1025
|
+
stopRecording();
|
|
1026
|
+
}, 1000);
|
|
584
1027
|
};
|
|
585
1028
|
React.useEffect(()=>{
|
|
586
1029
|
return ()=>{
|
|
587
|
-
if (
|
|
1030
|
+
if (progressTimer.current) clearInterval(progressTimer.current);
|
|
1031
|
+
if (taskTimer.current) clearTimeout(taskTimer.current);
|
|
588
1032
|
};
|
|
589
1033
|
}, []);
|
|
590
1034
|
if (!device) return /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
|
|
@@ -593,13 +1037,26 @@ function LivenessStep({ theme, locale, duration, onCapture, onBack }) {
|
|
|
593
1037
|
margin: 40
|
|
594
1038
|
}
|
|
595
1039
|
});
|
|
1040
|
+
const currentTask = tasks[taskIdx];
|
|
1041
|
+
const s = styles$3(theme);
|
|
596
1042
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
597
1043
|
style: s.container
|
|
1044
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1045
|
+
style: s.heading
|
|
598
1046
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
599
1047
|
style: s.title
|
|
600
|
-
}, locale.liveness_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1048
|
+
}, complete ? locale.liveness_complete : locale.liveness_title), !complete && /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
601
1049
|
style: s.subtitle
|
|
602
|
-
},
|
|
1050
|
+
}, locale.liveness_ready)), !complete && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1051
|
+
style: s.taskDots
|
|
1052
|
+
}, tasks.map((_, i)=>/*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1053
|
+
key: i,
|
|
1054
|
+
style: [
|
|
1055
|
+
s.dot,
|
|
1056
|
+
i < taskDone && s.dotDone,
|
|
1057
|
+
i === taskIdx && s.dotActive
|
|
1058
|
+
]
|
|
1059
|
+
}))), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
603
1060
|
style: s.viewport
|
|
604
1061
|
}, /*#__PURE__*/ React.createElement(reactNativeVisionCamera.Camera, {
|
|
605
1062
|
ref: cameraRef,
|
|
@@ -607,11 +1064,11 @@ function LivenessStep({ theme, locale, duration, onCapture, onBack }) {
|
|
|
607
1064
|
device: device,
|
|
608
1065
|
isActive: true,
|
|
609
1066
|
video: true,
|
|
610
|
-
audio:
|
|
1067
|
+
audio: false
|
|
611
1068
|
}), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
612
1069
|
style: s.ovalGuide,
|
|
613
1070
|
pointerEvents: "none"
|
|
614
|
-
}), recording && /*#__PURE__*/ React.createElement(
|
|
1071
|
+
}), recording && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
615
1072
|
style: s.recBadge
|
|
616
1073
|
}, /*#__PURE__*/ React.createElement(reactNative.Animated.View, {
|
|
617
1074
|
style: [
|
|
@@ -622,59 +1079,94 @@ function LivenessStep({ theme, locale, duration, onCapture, onBack }) {
|
|
|
622
1079
|
]
|
|
623
1080
|
}), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
624
1081
|
style: s.recText
|
|
625
|
-
}, "REC")), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
626
|
-
style: s.
|
|
1082
|
+
}, "REC")), currentTask && !complete && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1083
|
+
style: s.overlay
|
|
1084
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1085
|
+
style: s.overlayPill
|
|
1086
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1087
|
+
style: s.overlayNum
|
|
1088
|
+
}, taskIdx + 1, "/", tasks.length), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1089
|
+
style: s.overlayLabel
|
|
1090
|
+
}, currentTask.label)), started && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1091
|
+
style: s.barWrap
|
|
1092
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1093
|
+
style: [
|
|
1094
|
+
s.barFill,
|
|
1095
|
+
{
|
|
1096
|
+
width: `${progress}%`
|
|
1097
|
+
}
|
|
1098
|
+
]
|
|
1099
|
+
}))), complete && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1100
|
+
style: s.overlay
|
|
1101
|
+
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1102
|
+
style: s.overlayPill
|
|
627
1103
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
628
|
-
style: s.
|
|
629
|
-
},
|
|
630
|
-
style: s.
|
|
631
|
-
onPress:
|
|
1104
|
+
style: s.overlayLabel
|
|
1105
|
+
}, locale.liveness_complete)))), !started && !complete && currentTask && taskDone === 0 && /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
|
|
1106
|
+
style: s.btn,
|
|
1107
|
+
onPress: startTask,
|
|
632
1108
|
activeOpacity: 0.85
|
|
633
1109
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
634
1110
|
style: s.btnText
|
|
635
|
-
},
|
|
636
|
-
style: s.
|
|
637
|
-
|
|
638
|
-
activeOpacity: 0.75
|
|
639
|
-
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
640
|
-
style: s.btnSecondaryText
|
|
641
|
-
}, locale.selfie_retake_btn))));
|
|
1111
|
+
}, "Begin"), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1112
|
+
style: s.btnArrow
|
|
1113
|
+
}, " →")));
|
|
642
1114
|
}
|
|
643
1115
|
const styles$3 = (t)=>reactNative.StyleSheet.create({
|
|
644
1116
|
container: {
|
|
645
|
-
|
|
1117
|
+
flex: 1,
|
|
1118
|
+
padding: 20,
|
|
1119
|
+
gap: 12
|
|
1120
|
+
},
|
|
1121
|
+
heading: {
|
|
1122
|
+
gap: 4,
|
|
1123
|
+
alignItems: 'center'
|
|
646
1124
|
},
|
|
647
1125
|
title: {
|
|
648
1126
|
fontSize: 20,
|
|
649
1127
|
fontWeight: '700',
|
|
650
1128
|
color: t.text,
|
|
651
|
-
|
|
1129
|
+
textAlign: 'center',
|
|
1130
|
+
letterSpacing: -0.3
|
|
652
1131
|
},
|
|
653
1132
|
subtitle: {
|
|
654
|
-
fontSize:
|
|
1133
|
+
fontSize: 13,
|
|
655
1134
|
color: t.subtext,
|
|
656
|
-
lineHeight:
|
|
657
|
-
|
|
1135
|
+
lineHeight: 19,
|
|
1136
|
+
textAlign: 'center'
|
|
1137
|
+
},
|
|
1138
|
+
taskDots: {
|
|
1139
|
+
flexDirection: 'row',
|
|
1140
|
+
gap: 6
|
|
1141
|
+
},
|
|
1142
|
+
dot: {
|
|
1143
|
+
flex: 1,
|
|
1144
|
+
height: 2.5,
|
|
1145
|
+
borderRadius: 999,
|
|
1146
|
+
backgroundColor: t.border
|
|
1147
|
+
},
|
|
1148
|
+
dotDone: {
|
|
1149
|
+
backgroundColor: t.accent,
|
|
1150
|
+
opacity: 0.35
|
|
1151
|
+
},
|
|
1152
|
+
dotActive: {
|
|
1153
|
+
backgroundColor: t.accent
|
|
658
1154
|
},
|
|
659
1155
|
viewport: {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
borderRadius: t.radius,
|
|
1156
|
+
flex: 1,
|
|
1157
|
+
borderRadius: t.radius - 4,
|
|
663
1158
|
overflow: 'hidden',
|
|
664
1159
|
backgroundColor: '#000',
|
|
665
|
-
marginBottom: 16,
|
|
666
|
-
alignSelf: 'center',
|
|
667
|
-
maxWidth: 280,
|
|
668
1160
|
position: 'relative'
|
|
669
1161
|
},
|
|
670
1162
|
ovalGuide: {
|
|
671
1163
|
position: 'absolute',
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
bottom: '
|
|
1164
|
+
left: '21%',
|
|
1165
|
+
right: '21%',
|
|
1166
|
+
top: '6%',
|
|
1167
|
+
bottom: '6%',
|
|
676
1168
|
borderWidth: 2,
|
|
677
|
-
borderColor: 'rgba(255,255,255,0.
|
|
1169
|
+
borderColor: 'rgba(255,255,255,0.75)',
|
|
678
1170
|
borderRadius: 999
|
|
679
1171
|
},
|
|
680
1172
|
recBadge: {
|
|
@@ -683,144 +1175,134 @@ const styles$3 = (t)=>reactNative.StyleSheet.create({
|
|
|
683
1175
|
left: 10,
|
|
684
1176
|
flexDirection: 'row',
|
|
685
1177
|
alignItems: 'center',
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1178
|
+
gap: 5,
|
|
1179
|
+
backgroundColor: 'rgba(0,0,0,0.65)',
|
|
1180
|
+
borderWidth: 1,
|
|
1181
|
+
borderColor: 'rgba(239,68,68,0.5)',
|
|
1182
|
+
borderRadius: 5,
|
|
1183
|
+
paddingHorizontal: 8,
|
|
1184
|
+
paddingVertical: 4
|
|
691
1185
|
},
|
|
692
1186
|
recDot: {
|
|
693
|
-
width:
|
|
694
|
-
height:
|
|
695
|
-
borderRadius:
|
|
696
|
-
backgroundColor: '#
|
|
1187
|
+
width: 7,
|
|
1188
|
+
height: 7,
|
|
1189
|
+
borderRadius: 3.5,
|
|
1190
|
+
backgroundColor: '#ef4444'
|
|
697
1191
|
},
|
|
698
1192
|
recText: {
|
|
699
|
-
color: '#
|
|
700
|
-
fontSize:
|
|
701
|
-
fontWeight: '700'
|
|
1193
|
+
color: '#ef4444',
|
|
1194
|
+
fontSize: 11,
|
|
1195
|
+
fontWeight: '700',
|
|
1196
|
+
letterSpacing: 0.5
|
|
702
1197
|
},
|
|
703
|
-
|
|
1198
|
+
overlay: {
|
|
704
1199
|
position: 'absolute',
|
|
705
|
-
bottom:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1200
|
+
bottom: 0,
|
|
1201
|
+
left: 0,
|
|
1202
|
+
right: 0,
|
|
1203
|
+
paddingHorizontal: 12,
|
|
1204
|
+
paddingTop: 10,
|
|
1205
|
+
paddingBottom: 12,
|
|
1206
|
+
gap: 8,
|
|
1207
|
+
backgroundColor: 'transparent'
|
|
1208
|
+
},
|
|
1209
|
+
overlayPill: {
|
|
1210
|
+
flexDirection: 'row',
|
|
711
1211
|
alignItems: 'center',
|
|
712
|
-
|
|
1212
|
+
gap: 8
|
|
713
1213
|
},
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1214
|
+
overlayNum: {
|
|
1215
|
+
fontSize: 11,
|
|
1216
|
+
fontWeight: '700',
|
|
1217
|
+
color: 'rgba(255,255,255,0.55)',
|
|
1218
|
+
textTransform: 'uppercase',
|
|
1219
|
+
letterSpacing: 0.5
|
|
718
1220
|
},
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1221
|
+
overlayLabel: {
|
|
1222
|
+
fontSize: 15,
|
|
1223
|
+
fontWeight: '700',
|
|
1224
|
+
color: '#fff'
|
|
1225
|
+
},
|
|
1226
|
+
barWrap: {
|
|
1227
|
+
height: 3,
|
|
1228
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
1229
|
+
borderRadius: 999,
|
|
1230
|
+
overflow: 'hidden'
|
|
1231
|
+
},
|
|
1232
|
+
barFill: {
|
|
1233
|
+
height: '100%',
|
|
1234
|
+
backgroundColor: '#fff',
|
|
1235
|
+
borderRadius: 999
|
|
725
1236
|
},
|
|
726
|
-
|
|
727
|
-
backgroundColor: t.
|
|
728
|
-
borderRadius: t.radius,
|
|
729
|
-
paddingVertical:
|
|
1237
|
+
btn: {
|
|
1238
|
+
backgroundColor: t.accent,
|
|
1239
|
+
borderRadius: t.radius - 4,
|
|
1240
|
+
paddingVertical: 12,
|
|
1241
|
+
flexDirection: 'row',
|
|
730
1242
|
alignItems: 'center',
|
|
731
|
-
|
|
732
|
-
borderColor: t.border
|
|
1243
|
+
justifyContent: 'center'
|
|
733
1244
|
},
|
|
734
1245
|
btnText: {
|
|
735
|
-
color:
|
|
736
|
-
fontSize:
|
|
1246
|
+
color: t.accentInv,
|
|
1247
|
+
fontSize: 14,
|
|
737
1248
|
fontWeight: '600'
|
|
738
1249
|
},
|
|
739
|
-
|
|
740
|
-
color: t.
|
|
741
|
-
fontSize:
|
|
1250
|
+
btnArrow: {
|
|
1251
|
+
color: t.accentInv,
|
|
1252
|
+
fontSize: 14,
|
|
742
1253
|
fontWeight: '600'
|
|
743
1254
|
}
|
|
744
1255
|
});
|
|
745
1256
|
|
|
746
1257
|
function ProcessingStep({ theme, locale }) {
|
|
747
|
-
const spinAnim = React.useRef(new reactNative.Animated.Value(0)).current;
|
|
748
1258
|
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
1259
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
769
1260
|
style: s.container
|
|
770
|
-
}, /*#__PURE__*/ React.createElement(reactNative.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
{
|
|
774
|
-
transform: [
|
|
775
|
-
{
|
|
776
|
-
rotate: spin
|
|
777
|
-
}
|
|
778
|
-
]
|
|
779
|
-
}
|
|
780
|
-
]
|
|
1261
|
+
}, /*#__PURE__*/ React.createElement(reactNative.ActivityIndicator, {
|
|
1262
|
+
color: theme.accent,
|
|
1263
|
+
size: "large"
|
|
781
1264
|
}), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
782
1265
|
style: s.title
|
|
783
1266
|
}, locale.processing_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
784
1267
|
style: s.subtitle
|
|
785
|
-
},
|
|
1268
|
+
}, "This usually takes under 10 seconds."));
|
|
786
1269
|
}
|
|
787
1270
|
const styles$2 = (t)=>reactNative.StyleSheet.create({
|
|
788
1271
|
container: {
|
|
789
|
-
|
|
1272
|
+
flex: 1,
|
|
790
1273
|
alignItems: 'center',
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
width: 52,
|
|
795
|
-
height: 52,
|
|
796
|
-
borderRadius: 26,
|
|
797
|
-
borderWidth: 4,
|
|
798
|
-
borderColor: t.border,
|
|
799
|
-
borderTopColor: t.accent,
|
|
800
|
-
marginBottom: 8
|
|
1274
|
+
justifyContent: 'center',
|
|
1275
|
+
gap: 16,
|
|
1276
|
+
padding: 32
|
|
801
1277
|
},
|
|
802
1278
|
title: {
|
|
803
|
-
fontSize:
|
|
1279
|
+
fontSize: 16,
|
|
804
1280
|
fontWeight: '700',
|
|
805
1281
|
color: t.text,
|
|
806
1282
|
textAlign: 'center'
|
|
807
1283
|
},
|
|
808
1284
|
subtitle: {
|
|
809
|
-
fontSize:
|
|
1285
|
+
fontSize: 13,
|
|
810
1286
|
color: t.subtext,
|
|
811
1287
|
textAlign: 'center'
|
|
812
1288
|
}
|
|
813
1289
|
});
|
|
814
1290
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1291
|
+
// Unicode block characters — no emoji, render cleanly on all platforms
|
|
1292
|
+
const ICON = {
|
|
1293
|
+
pass: '\u2714',
|
|
1294
|
+
fail: '\u2718',
|
|
1295
|
+
review: '\u29D6'
|
|
819
1296
|
};
|
|
820
1297
|
const ICON_BG = {
|
|
821
|
-
pass: '#
|
|
1298
|
+
pass: '#dcfce7',
|
|
822
1299
|
fail: '#fee2e2',
|
|
823
|
-
review: '#
|
|
1300
|
+
review: '#fef9c3'
|
|
1301
|
+
};
|
|
1302
|
+
const ICON_COLOR = {
|
|
1303
|
+
pass: '#16a34a',
|
|
1304
|
+
fail: '#dc2626',
|
|
1305
|
+
review: '#ca8a04'
|
|
824
1306
|
};
|
|
825
1307
|
function ResultStep({ theme, locale, result, error, onRetry }) {
|
|
826
1308
|
const s = styles$1(theme);
|
|
@@ -829,14 +1311,19 @@ function ResultStep({ theme, locale, result, error, onRetry }) {
|
|
|
829
1311
|
style: s.container
|
|
830
1312
|
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
831
1313
|
style: [
|
|
832
|
-
s.
|
|
1314
|
+
s.iconBox,
|
|
833
1315
|
{
|
|
834
|
-
backgroundColor:
|
|
1316
|
+
backgroundColor: ICON_BG.fail
|
|
835
1317
|
}
|
|
836
1318
|
]
|
|
837
1319
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
838
|
-
style:
|
|
839
|
-
|
|
1320
|
+
style: [
|
|
1321
|
+
s.icon,
|
|
1322
|
+
{
|
|
1323
|
+
color: ICON_COLOR.fail
|
|
1324
|
+
}
|
|
1325
|
+
]
|
|
1326
|
+
}, ICON.fail)), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
840
1327
|
style: s.title
|
|
841
1328
|
}, locale.result_fail_title), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
842
1329
|
style: s.body
|
|
@@ -846,8 +1333,9 @@ function ResultStep({ theme, locale, result, error, onRetry }) {
|
|
|
846
1333
|
activeOpacity: 0.85
|
|
847
1334
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
848
1335
|
style: s.btnText
|
|
849
|
-
},
|
|
1336
|
+
}, "Try again")));
|
|
850
1337
|
}
|
|
1338
|
+
const v = result.verdict;
|
|
851
1339
|
const titles = {
|
|
852
1340
|
pass: locale.result_pass_title,
|
|
853
1341
|
fail: locale.result_fail_title,
|
|
@@ -858,85 +1346,148 @@ function ResultStep({ theme, locale, result, error, onRetry }) {
|
|
|
858
1346
|
fail: locale.result_fail_body,
|
|
859
1347
|
review: locale.result_review_body
|
|
860
1348
|
};
|
|
1349
|
+
const rows = [
|
|
1350
|
+
{
|
|
1351
|
+
label: 'Verdict',
|
|
1352
|
+
value: v.charAt(0).toUpperCase() + v.slice(1),
|
|
1353
|
+
ok: v === 'pass',
|
|
1354
|
+
bad: v === 'fail'
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
label: 'Risk score',
|
|
1358
|
+
value: String(result.score ?? '—')
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
label: 'Face match',
|
|
1362
|
+
value: result.faceMatch ? 'Confirmed' : 'Not confirmed',
|
|
1363
|
+
ok: result.faceMatch,
|
|
1364
|
+
bad: !result.faceMatch
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
label: 'Liveness',
|
|
1368
|
+
value: result.isLive ? 'Confirmed' : 'Not confirmed',
|
|
1369
|
+
ok: result.isLive,
|
|
1370
|
+
bad: !result.isLive
|
|
1371
|
+
}
|
|
1372
|
+
];
|
|
861
1373
|
return /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
862
1374
|
style: s.container
|
|
863
1375
|
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
864
1376
|
style: [
|
|
865
|
-
s.
|
|
1377
|
+
s.iconBox,
|
|
866
1378
|
{
|
|
867
|
-
backgroundColor: ICON_BG[
|
|
1379
|
+
backgroundColor: ICON_BG[v]
|
|
868
1380
|
}
|
|
869
1381
|
]
|
|
870
1382
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
871
|
-
style:
|
|
872
|
-
|
|
1383
|
+
style: [
|
|
1384
|
+
s.icon,
|
|
1385
|
+
{
|
|
1386
|
+
color: ICON_COLOR[v]
|
|
1387
|
+
}
|
|
1388
|
+
]
|
|
1389
|
+
}, ICON[v])), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
873
1390
|
style: s.title
|
|
874
|
-
}, titles[
|
|
1391
|
+
}, titles[v]), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
875
1392
|
style: s.body
|
|
876
|
-
}, bodies[
|
|
1393
|
+
}, bodies[v]), /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1394
|
+
style: s.details
|
|
1395
|
+
}, rows.map((row)=>/*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1396
|
+
key: row.label,
|
|
1397
|
+
style: s.row
|
|
1398
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1399
|
+
style: s.rowLabel
|
|
1400
|
+
}, row.label), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1401
|
+
style: [
|
|
1402
|
+
s.rowVal,
|
|
1403
|
+
row.ok && s.rowOk,
|
|
1404
|
+
row.bad && s.rowBad
|
|
1405
|
+
]
|
|
1406
|
+
}, row.value)))), v === 'fail' && /*#__PURE__*/ React.createElement(reactNative.TouchableOpacity, {
|
|
877
1407
|
style: s.btn,
|
|
878
1408
|
onPress: onRetry,
|
|
879
1409
|
activeOpacity: 0.85
|
|
880
1410
|
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
881
1411
|
style: s.btnText
|
|
882
|
-
},
|
|
1412
|
+
}, "Try again")));
|
|
883
1413
|
}
|
|
884
1414
|
const styles$1 = (t)=>reactNative.StyleSheet.create({
|
|
885
1415
|
container: {
|
|
886
|
-
|
|
1416
|
+
flex: 1,
|
|
1417
|
+
padding: 28,
|
|
887
1418
|
alignItems: 'center',
|
|
888
|
-
gap:
|
|
1419
|
+
gap: 14,
|
|
1420
|
+
justifyContent: 'center'
|
|
889
1421
|
},
|
|
890
|
-
|
|
891
|
-
width:
|
|
892
|
-
height:
|
|
893
|
-
borderRadius:
|
|
1422
|
+
iconBox: {
|
|
1423
|
+
width: 52,
|
|
1424
|
+
height: 52,
|
|
1425
|
+
borderRadius: 14,
|
|
894
1426
|
alignItems: 'center',
|
|
895
1427
|
justifyContent: 'center',
|
|
896
|
-
marginBottom:
|
|
1428
|
+
marginBottom: 4
|
|
897
1429
|
},
|
|
898
1430
|
icon: {
|
|
899
|
-
fontSize:
|
|
1431
|
+
fontSize: 22,
|
|
1432
|
+
fontWeight: '700'
|
|
900
1433
|
},
|
|
901
1434
|
title: {
|
|
902
1435
|
fontSize: 20,
|
|
903
1436
|
fontWeight: '700',
|
|
904
1437
|
color: t.text,
|
|
905
|
-
textAlign: 'center'
|
|
1438
|
+
textAlign: 'center',
|
|
1439
|
+
letterSpacing: -0.3
|
|
906
1440
|
},
|
|
907
1441
|
body: {
|
|
908
|
-
fontSize:
|
|
1442
|
+
fontSize: 13.5,
|
|
909
1443
|
color: t.subtext,
|
|
910
1444
|
textAlign: 'center',
|
|
911
|
-
lineHeight:
|
|
1445
|
+
lineHeight: 19
|
|
1446
|
+
},
|
|
1447
|
+
details: {
|
|
1448
|
+
width: '100%',
|
|
1449
|
+
gap: 6
|
|
1450
|
+
},
|
|
1451
|
+
row: {
|
|
1452
|
+
flexDirection: 'row',
|
|
1453
|
+
justifyContent: 'space-between',
|
|
1454
|
+
alignItems: 'center',
|
|
1455
|
+
paddingVertical: 9,
|
|
1456
|
+
paddingHorizontal: 12,
|
|
1457
|
+
backgroundColor: t.surface,
|
|
1458
|
+
borderRadius: 8
|
|
1459
|
+
},
|
|
1460
|
+
rowLabel: {
|
|
1461
|
+
fontSize: 12.5,
|
|
1462
|
+
color: t.subtext
|
|
1463
|
+
},
|
|
1464
|
+
rowVal: {
|
|
1465
|
+
fontSize: 12.5,
|
|
1466
|
+
fontWeight: '600',
|
|
1467
|
+
color: t.text
|
|
1468
|
+
},
|
|
1469
|
+
rowOk: {
|
|
1470
|
+
color: '#16a34a'
|
|
1471
|
+
},
|
|
1472
|
+
rowBad: {
|
|
1473
|
+
color: '#dc2626'
|
|
912
1474
|
},
|
|
913
1475
|
btn: {
|
|
914
|
-
|
|
1476
|
+
width: '100%',
|
|
915
1477
|
backgroundColor: t.accent,
|
|
916
|
-
borderRadius: t.radius,
|
|
917
|
-
paddingVertical:
|
|
918
|
-
|
|
919
|
-
|
|
1478
|
+
borderRadius: t.radius - 4,
|
|
1479
|
+
paddingVertical: 12,
|
|
1480
|
+
alignItems: 'center',
|
|
1481
|
+
marginTop: 4
|
|
920
1482
|
},
|
|
921
1483
|
btnText: {
|
|
922
|
-
color:
|
|
923
|
-
fontSize:
|
|
1484
|
+
color: t.accentInv,
|
|
1485
|
+
fontSize: 14,
|
|
924
1486
|
fontWeight: '600'
|
|
925
1487
|
}
|
|
926
1488
|
});
|
|
927
1489
|
|
|
928
|
-
|
|
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 }) {
|
|
1490
|
+
function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference, sandbox = false, livenessDuration = 3000, livenessTaskCount = 2, documentType = 'national_id', steps: stepsProp, theme: themeProp, locale: localeProp, onComplete, onError }) {
|
|
940
1491
|
const [step, setStep] = React.useState('intro');
|
|
941
1492
|
const [capture, setCapture] = React.useState({});
|
|
942
1493
|
const [result, setResult] = React.useState(null);
|
|
@@ -952,48 +1503,79 @@ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference,
|
|
|
952
1503
|
]);
|
|
953
1504
|
const { requestPermissions } = usePermissions();
|
|
954
1505
|
const s = styles(theme);
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1506
|
+
// Build the ordered flow from the steps prop
|
|
1507
|
+
const flow = React.useMemo(()=>{
|
|
1508
|
+
const wanted = stepsProp ?? [
|
|
1509
|
+
'document',
|
|
1510
|
+
'selfie',
|
|
1511
|
+
'liveness'
|
|
1512
|
+
];
|
|
1513
|
+
const out = [];
|
|
1514
|
+
if (wanted.includes('document')) {
|
|
1515
|
+
out.push('doc-front');
|
|
1516
|
+
out.push('doc-back');
|
|
1517
|
+
}
|
|
1518
|
+
if (wanted.includes('selfie')) out.push('selfie');
|
|
1519
|
+
if (wanted.includes('liveness')) out.push('liveness');
|
|
1520
|
+
out.push('processing');
|
|
1521
|
+
return out;
|
|
1522
|
+
}, [
|
|
1523
|
+
stepsProp
|
|
1524
|
+
]);
|
|
1525
|
+
const next = React.useCallback((current)=>{
|
|
1526
|
+
const idx = flow.indexOf(current);
|
|
1527
|
+
const nextStep = flow[idx + 1] ?? 'processing';
|
|
1528
|
+
goto(nextStep);
|
|
1529
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1530
|
+
}, [
|
|
1531
|
+
flow
|
|
1532
|
+
]);
|
|
1533
|
+
const goto = React.useCallback(async (s)=>{
|
|
1534
|
+
if (s === 'doc-front' || s === 'doc-back' || s === 'selfie' || s === 'liveness') {
|
|
963
1535
|
const granted = await requestPermissions();
|
|
964
1536
|
if (!granted) {
|
|
965
1537
|
setError(locale.error_camera_denied);
|
|
966
1538
|
return;
|
|
967
1539
|
}
|
|
968
1540
|
}
|
|
969
|
-
setStep(
|
|
970
|
-
if (
|
|
971
|
-
submit();
|
|
972
|
-
}
|
|
1541
|
+
setStep(s);
|
|
1542
|
+
if (s === 'processing') submit();
|
|
973
1543
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
974
1544
|
}, [
|
|
975
1545
|
capture,
|
|
976
1546
|
locale
|
|
977
1547
|
]);
|
|
1548
|
+
const reset = React.useCallback(()=>{
|
|
1549
|
+
setStep('intro');
|
|
1550
|
+
setCapture({});
|
|
1551
|
+
setResult(null);
|
|
1552
|
+
setError(null);
|
|
1553
|
+
}, []);
|
|
978
1554
|
const submit = React.useCallback(async ()=>{
|
|
979
|
-
const {
|
|
980
|
-
if (!documentUri || !selfieUri) {
|
|
981
|
-
setError('Missing captures — please restart.');
|
|
982
|
-
setStep('result');
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
1555
|
+
const { docFrontUri, docBackUri, selfieUri, videoUri } = capture;
|
|
985
1556
|
const base = (backendUrl ?? 'https://api.identity.kernaq.com/v1').replace(/\/$/, '');
|
|
986
1557
|
const form = new FormData();
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1558
|
+
if (docFrontUri) {
|
|
1559
|
+
form.append('document', {
|
|
1560
|
+
uri: docFrontUri,
|
|
1561
|
+
name: 'doc_front.jpg',
|
|
1562
|
+
type: 'image/jpeg'
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
if (docBackUri) {
|
|
1566
|
+
form.append('document_back', {
|
|
1567
|
+
uri: docBackUri,
|
|
1568
|
+
name: 'doc_back.jpg',
|
|
1569
|
+
type: 'image/jpeg'
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
if (selfieUri) {
|
|
1573
|
+
form.append('selfie', {
|
|
1574
|
+
uri: selfieUri,
|
|
1575
|
+
name: 'selfie.jpg',
|
|
1576
|
+
type: 'image/jpeg'
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
997
1579
|
if (videoUri) {
|
|
998
1580
|
form.append('video', {
|
|
999
1581
|
uri: videoUri,
|
|
@@ -1001,7 +1583,7 @@ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference,
|
|
|
1001
1583
|
type: 'video/mp4'
|
|
1002
1584
|
});
|
|
1003
1585
|
}
|
|
1004
|
-
form.append('document_type',
|
|
1586
|
+
form.append('document_type', documentType);
|
|
1005
1587
|
form.append('country', country);
|
|
1006
1588
|
form.append('reference', reference ?? `kq_${Date.now()}`);
|
|
1007
1589
|
if (sandbox) form.append('sandbox', 'true');
|
|
@@ -1032,6 +1614,7 @@ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference,
|
|
|
1032
1614
|
} catch (err) {
|
|
1033
1615
|
const msg = err.message;
|
|
1034
1616
|
setError(`${locale.error_network} ${msg}`);
|
|
1617
|
+
setResult(null);
|
|
1035
1618
|
setStep('result');
|
|
1036
1619
|
onError?.({
|
|
1037
1620
|
code: 'network_error',
|
|
@@ -1042,76 +1625,56 @@ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference,
|
|
|
1042
1625
|
}, [
|
|
1043
1626
|
capture
|
|
1044
1627
|
]);
|
|
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
1628
|
return /*#__PURE__*/ React.createElement(reactNative.Modal, {
|
|
1057
1629
|
visible: visible,
|
|
1058
1630
|
animationType: "slide",
|
|
1059
1631
|
presentationStyle: "pageSheet",
|
|
1060
|
-
onRequestClose:
|
|
1632
|
+
onRequestClose: ()=>{}
|
|
1061
1633
|
}, /*#__PURE__*/ React.createElement(reactNative.SafeAreaView, {
|
|
1062
1634
|
style: [
|
|
1063
|
-
s.
|
|
1635
|
+
s.safe,
|
|
1064
1636
|
{
|
|
1065
1637
|
backgroundColor: theme.bg
|
|
1066
1638
|
}
|
|
1067
1639
|
]
|
|
1068
1640
|
}, /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1069
|
-
style: s.
|
|
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
|
|
1641
|
+
style: s.body
|
|
1102
1642
|
}, step === 'intro' && /*#__PURE__*/ React.createElement(IntroStep, {
|
|
1103
1643
|
theme: theme,
|
|
1104
1644
|
locale: locale,
|
|
1105
|
-
onStart: ()=>
|
|
1106
|
-
|
|
1645
|
+
onStart: ()=>{
|
|
1646
|
+
const first = flow[0];
|
|
1647
|
+
if (first) goto(first);
|
|
1648
|
+
}
|
|
1649
|
+
}), step === 'doc-front' && /*#__PURE__*/ React.createElement(DocumentStep, {
|
|
1107
1650
|
theme: theme,
|
|
1108
1651
|
locale: locale,
|
|
1652
|
+
side: "front",
|
|
1109
1653
|
onCapture: (uri)=>{
|
|
1110
1654
|
setCapture((c)=>({
|
|
1111
1655
|
...c,
|
|
1112
|
-
|
|
1656
|
+
docFrontUri: uri
|
|
1113
1657
|
}));
|
|
1114
|
-
|
|
1658
|
+
next('doc-front');
|
|
1659
|
+
}
|
|
1660
|
+
}), step === 'doc-back' && /*#__PURE__*/ React.createElement(DocumentStep, {
|
|
1661
|
+
theme: theme,
|
|
1662
|
+
locale: locale,
|
|
1663
|
+
side: "back",
|
|
1664
|
+
frontUri: capture.docFrontUri,
|
|
1665
|
+
onCapture: (uri)=>{
|
|
1666
|
+
setCapture((c)=>({
|
|
1667
|
+
...c,
|
|
1668
|
+
docBackUri: uri
|
|
1669
|
+
}));
|
|
1670
|
+
next('doc-back');
|
|
1671
|
+
},
|
|
1672
|
+
onRetakeFront: ()=>{
|
|
1673
|
+
setCapture((c)=>({
|
|
1674
|
+
...c,
|
|
1675
|
+
docFrontUri: undefined
|
|
1676
|
+
}));
|
|
1677
|
+
goto('doc-front');
|
|
1115
1678
|
}
|
|
1116
1679
|
}), step === 'selfie' && /*#__PURE__*/ React.createElement(SelfieStep, {
|
|
1117
1680
|
theme: theme,
|
|
@@ -1121,21 +1684,20 @@ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference,
|
|
|
1121
1684
|
...c,
|
|
1122
1685
|
selfieUri: uri
|
|
1123
1686
|
}));
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
onBack: ()=>setStep('document')
|
|
1687
|
+
next('selfie');
|
|
1688
|
+
}
|
|
1127
1689
|
}), step === 'liveness' && /*#__PURE__*/ React.createElement(LivenessStep, {
|
|
1128
1690
|
theme: theme,
|
|
1129
1691
|
locale: locale,
|
|
1130
1692
|
duration: livenessDuration,
|
|
1693
|
+
taskCount: livenessTaskCount,
|
|
1131
1694
|
onCapture: (uri)=>{
|
|
1132
1695
|
setCapture((c)=>({
|
|
1133
1696
|
...c,
|
|
1134
1697
|
videoUri: uri
|
|
1135
1698
|
}));
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
onBack: ()=>setStep('selfie')
|
|
1699
|
+
next('liveness');
|
|
1700
|
+
}
|
|
1139
1701
|
}), step === 'processing' && /*#__PURE__*/ React.createElement(ProcessingStep, {
|
|
1140
1702
|
theme: theme,
|
|
1141
1703
|
locale: locale
|
|
@@ -1146,55 +1708,45 @@ function KernaqVerify({ visible, apiKey, backendUrl, country = 'KEN', reference,
|
|
|
1146
1708
|
error: error,
|
|
1147
1709
|
onRetry: ()=>{
|
|
1148
1710
|
reset();
|
|
1149
|
-
|
|
1711
|
+
goto('intro');
|
|
1150
1712
|
}
|
|
1151
|
-
}))
|
|
1713
|
+
})), step !== 'processing' && /*#__PURE__*/ React.createElement(reactNative.View, {
|
|
1714
|
+
style: s.footer
|
|
1715
|
+
}, /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1716
|
+
style: s.footerText
|
|
1717
|
+
}, "Secured by "), /*#__PURE__*/ React.createElement(reactNative.Text, {
|
|
1718
|
+
style: [
|
|
1719
|
+
s.footerText,
|
|
1720
|
+
{
|
|
1721
|
+
fontWeight: '600',
|
|
1722
|
+
color: theme.text
|
|
1723
|
+
}
|
|
1724
|
+
]
|
|
1725
|
+
}, "Kernaq"))));
|
|
1152
1726
|
}
|
|
1153
1727
|
const styles = (t)=>reactNative.StyleSheet.create({
|
|
1154
|
-
|
|
1728
|
+
safe: {
|
|
1729
|
+
flex: 1
|
|
1730
|
+
},
|
|
1731
|
+
body: {
|
|
1155
1732
|
flex: 1
|
|
1156
1733
|
},
|
|
1157
|
-
|
|
1734
|
+
footer: {
|
|
1158
1735
|
flexDirection: 'row',
|
|
1159
1736
|
alignItems: 'center',
|
|
1160
|
-
justifyContent: '
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1737
|
+
justifyContent: 'center',
|
|
1738
|
+
paddingVertical: 10,
|
|
1739
|
+
paddingBottom: reactNative.Platform.OS === 'ios' ? 4 : 10,
|
|
1740
|
+
borderTopWidth: reactNative.StyleSheet.hairlineWidth,
|
|
1741
|
+
borderTopColor: t.border
|
|
1174
1742
|
},
|
|
1175
|
-
|
|
1176
|
-
fontSize:
|
|
1743
|
+
footerText: {
|
|
1744
|
+
fontSize: 11,
|
|
1177
1745
|
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
1746
|
}
|
|
1197
1747
|
});
|
|
1198
1748
|
|
|
1199
1749
|
exports.KernaqVerify = KernaqVerify;
|
|
1750
|
+
exports.analyseImageUri = analyseImageUri;
|
|
1751
|
+
exports.analyseSelfieFrame = analyseSelfieFrame;
|
|
1200
1752
|
//# sourceMappingURL=index.js.map
|