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