@koraidv/react 1.7.6 → 1.7.8
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.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +237 -52
- package/dist/index.mjs +271 -86
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -205,6 +205,37 @@ interface LivenessScreenProps {
|
|
|
205
205
|
onComplete: () => Promise<any>;
|
|
206
206
|
onCancel: () => void;
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Web liveness screen with a real front-facing camera.
|
|
210
|
+
*
|
|
211
|
+
* Before v1.7.7 this was a stub: a static oval guide + a "Complete
|
|
212
|
+
* Challenge" button that submitted a blank 100×100 white canvas. The
|
|
213
|
+
* camera was never wired up — Web shipped without a real liveness
|
|
214
|
+
* implementation while iOS + Android had full liveness from day one.
|
|
215
|
+
* Found and called out by Luckycat's first end-to-end integration on
|
|
216
|
+
* 2026-05-29.
|
|
217
|
+
*
|
|
218
|
+
* This implementation pairs a real `getUserMedia` camera feed with the
|
|
219
|
+
* server's existing liveness pipeline (ml-service detects whether the
|
|
220
|
+
* submitted frame shows the requested gesture). Per-challenge flow:
|
|
221
|
+
*
|
|
222
|
+
* 1. Render the front camera in the oval guide (object-fit cover,
|
|
223
|
+
* circular clip — same visual shape as the previous stub).
|
|
224
|
+
* 2. Show the challenge instruction (Smile / Turn left / Blink / ...).
|
|
225
|
+
* 3. Run a 3-second countdown so the user has time to perform the
|
|
226
|
+
* gesture.
|
|
227
|
+
* 4. When the countdown hits zero, capture a frame from the video
|
|
228
|
+
* and post it to /verifications/{id}/liveness/challenge with the
|
|
229
|
+
* challenge type. The hook's submitChallenge advances state on
|
|
230
|
+
* pass; on fail the same challenge stays current and the
|
|
231
|
+
* countdown re-arms for retry.
|
|
232
|
+
*
|
|
233
|
+
* Client-side gesture detection (MediaPipe Face Mesh + auto-capture
|
|
234
|
+
* when the gesture is satisfied) is the planned follow-up — that would
|
|
235
|
+
* close the UX gap with iOS, where the user doesn't have to time
|
|
236
|
+
* themselves against a countdown. Today's ship is "real camera, real
|
|
237
|
+
* frames, server decides," which is the parity floor.
|
|
238
|
+
*/
|
|
208
239
|
declare function LivenessScreen({ session, currentChallenge, completedChallenges, onChallengeComplete, onStart, onComplete, onCancel, }: LivenessScreenProps): react_jsx_runtime.JSX.Element;
|
|
209
240
|
|
|
210
241
|
type ResultPageMode = 'detailed' | 'simplified';
|
package/dist/index.d.ts
CHANGED
|
@@ -205,6 +205,37 @@ interface LivenessScreenProps {
|
|
|
205
205
|
onComplete: () => Promise<any>;
|
|
206
206
|
onCancel: () => void;
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Web liveness screen with a real front-facing camera.
|
|
210
|
+
*
|
|
211
|
+
* Before v1.7.7 this was a stub: a static oval guide + a "Complete
|
|
212
|
+
* Challenge" button that submitted a blank 100×100 white canvas. The
|
|
213
|
+
* camera was never wired up — Web shipped without a real liveness
|
|
214
|
+
* implementation while iOS + Android had full liveness from day one.
|
|
215
|
+
* Found and called out by Luckycat's first end-to-end integration on
|
|
216
|
+
* 2026-05-29.
|
|
217
|
+
*
|
|
218
|
+
* This implementation pairs a real `getUserMedia` camera feed with the
|
|
219
|
+
* server's existing liveness pipeline (ml-service detects whether the
|
|
220
|
+
* submitted frame shows the requested gesture). Per-challenge flow:
|
|
221
|
+
*
|
|
222
|
+
* 1. Render the front camera in the oval guide (object-fit cover,
|
|
223
|
+
* circular clip — same visual shape as the previous stub).
|
|
224
|
+
* 2. Show the challenge instruction (Smile / Turn left / Blink / ...).
|
|
225
|
+
* 3. Run a 3-second countdown so the user has time to perform the
|
|
226
|
+
* gesture.
|
|
227
|
+
* 4. When the countdown hits zero, capture a frame from the video
|
|
228
|
+
* and post it to /verifications/{id}/liveness/challenge with the
|
|
229
|
+
* challenge type. The hook's submitChallenge advances state on
|
|
230
|
+
* pass; on fail the same challenge stays current and the
|
|
231
|
+
* countdown re-arms for retry.
|
|
232
|
+
*
|
|
233
|
+
* Client-side gesture detection (MediaPipe Face Mesh + auto-capture
|
|
234
|
+
* when the gesture is satisfied) is the planned follow-up — that would
|
|
235
|
+
* close the UX gap with iOS, where the user doesn't have to time
|
|
236
|
+
* themselves against a countdown. Today's ship is "real camera, real
|
|
237
|
+
* frames, server decides," which is the parity floor.
|
|
238
|
+
*/
|
|
208
239
|
declare function LivenessScreen({ session, currentChallenge, completedChallenges, onChallengeComplete, onStart, onComplete, onCancel, }: LivenessScreenProps): react_jsx_runtime.JSX.Element;
|
|
209
240
|
|
|
210
241
|
type ResultPageMode = 'detailed' | 'simplified';
|
package/dist/index.js
CHANGED
|
@@ -330,6 +330,15 @@ function useKoraIDV() {
|
|
|
330
330
|
return null;
|
|
331
331
|
}
|
|
332
332
|
}, [sdk]);
|
|
333
|
+
const completionFiredRef = (0, import_react2.useRef)(false);
|
|
334
|
+
(0, import_react2.useEffect)(() => {
|
|
335
|
+
if (state.step === "processing" && !completionFiredRef.current) {
|
|
336
|
+
completionFiredRef.current = true;
|
|
337
|
+
complete();
|
|
338
|
+
} else if (state.step !== "processing" && state.step !== "complete") {
|
|
339
|
+
completionFiredRef.current = false;
|
|
340
|
+
}
|
|
341
|
+
}, [state.step, complete]);
|
|
333
342
|
const cancel = (0, import_react2.useCallback)(() => {
|
|
334
343
|
sdk.reset();
|
|
335
344
|
setState({
|
|
@@ -2341,7 +2350,13 @@ function LivenessScreen({
|
|
|
2341
2350
|
onComplete,
|
|
2342
2351
|
onCancel
|
|
2343
2352
|
}) {
|
|
2353
|
+
const videoRef = (0, import_react8.useRef)(null);
|
|
2354
|
+
const canvasRef = (0, import_react8.useRef)(null);
|
|
2355
|
+
const [stream, setStream] = (0, import_react8.useState)(null);
|
|
2356
|
+
const [cameraError, setCameraError] = (0, import_react8.useState)(null);
|
|
2357
|
+
const [phase, setPhase] = (0, import_react8.useState)("preparing");
|
|
2344
2358
|
const [countdown, setCountdown] = (0, import_react8.useState)(3);
|
|
2359
|
+
const [capturing, setCapturing] = (0, import_react8.useState)(false);
|
|
2345
2360
|
(0, import_react8.useEffect)(() => {
|
|
2346
2361
|
injectKeyframes();
|
|
2347
2362
|
}, []);
|
|
@@ -2349,24 +2364,95 @@ function LivenessScreen({
|
|
|
2349
2364
|
if (!session) onStart();
|
|
2350
2365
|
}, [session, onStart]);
|
|
2351
2366
|
(0, import_react8.useEffect)(() => {
|
|
2352
|
-
|
|
2353
|
-
|
|
2367
|
+
let mounted = true;
|
|
2368
|
+
async function startCamera() {
|
|
2369
|
+
try {
|
|
2370
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
2371
|
+
video: {
|
|
2372
|
+
facingMode: "user",
|
|
2373
|
+
width: { ideal: 720 },
|
|
2374
|
+
height: { ideal: 720 }
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
if (!mounted) {
|
|
2378
|
+
mediaStream.getTracks().forEach((t) => t.stop());
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
setStream(mediaStream);
|
|
2382
|
+
if (videoRef.current) {
|
|
2383
|
+
videoRef.current.srcObject = mediaStream;
|
|
2384
|
+
}
|
|
2385
|
+
} catch {
|
|
2386
|
+
if (mounted) {
|
|
2387
|
+
setCameraError(
|
|
2388
|
+
"Camera access denied. Please enable camera permissions and try again."
|
|
2389
|
+
);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2354
2392
|
}
|
|
2355
|
-
|
|
2393
|
+
startCamera();
|
|
2394
|
+
return () => {
|
|
2395
|
+
mounted = false;
|
|
2396
|
+
};
|
|
2397
|
+
}, []);
|
|
2398
|
+
(0, import_react8.useEffect)(() => {
|
|
2399
|
+
return () => {
|
|
2400
|
+
stream?.getTracks().forEach((t) => t.stop());
|
|
2401
|
+
};
|
|
2402
|
+
}, [stream]);
|
|
2356
2403
|
(0, import_react8.useEffect)(() => {
|
|
2357
2404
|
if (!currentChallenge) return;
|
|
2405
|
+
setPhase("preparing");
|
|
2358
2406
|
setCountdown(3);
|
|
2359
|
-
const interval = setInterval(() => {
|
|
2360
|
-
setCountdown((c) => {
|
|
2361
|
-
if (c <= 1) {
|
|
2362
|
-
clearInterval(interval);
|
|
2363
|
-
return 0;
|
|
2364
|
-
}
|
|
2365
|
-
return c - 1;
|
|
2366
|
-
});
|
|
2367
|
-
}, 1e3);
|
|
2368
|
-
return () => clearInterval(interval);
|
|
2369
2407
|
}, [currentChallenge?.id]);
|
|
2408
|
+
const captureFrame = (0, import_react8.useCallback)(async () => {
|
|
2409
|
+
if (!currentChallenge || !videoRef.current || !canvasRef.current || capturing) {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const video = videoRef.current;
|
|
2413
|
+
const canvas = canvasRef.current;
|
|
2414
|
+
const ctx = canvas.getContext("2d");
|
|
2415
|
+
if (!ctx || video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
2416
|
+
setCapturing(true);
|
|
2417
|
+
canvas.width = video.videoWidth;
|
|
2418
|
+
canvas.height = video.videoHeight;
|
|
2419
|
+
ctx.drawImage(video, 0, 0);
|
|
2420
|
+
canvas.toBlob(
|
|
2421
|
+
async (blob) => {
|
|
2422
|
+
if (blob) {
|
|
2423
|
+
await onChallengeComplete(blob);
|
|
2424
|
+
}
|
|
2425
|
+
setCapturing(false);
|
|
2426
|
+
},
|
|
2427
|
+
"image/jpeg",
|
|
2428
|
+
0.85
|
|
2429
|
+
);
|
|
2430
|
+
}, [currentChallenge, capturing, onChallengeComplete]);
|
|
2431
|
+
(0, import_react8.useEffect)(() => {
|
|
2432
|
+
if (!currentChallenge || capturing) return;
|
|
2433
|
+
if (countdown === 0) {
|
|
2434
|
+
if (phase === "preparing") {
|
|
2435
|
+
setPhase("capturing");
|
|
2436
|
+
setCountdown(3);
|
|
2437
|
+
} else {
|
|
2438
|
+
captureFrame();
|
|
2439
|
+
}
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
const t = setTimeout(() => setCountdown((c) => c - 1), 1e3);
|
|
2443
|
+
return () => clearTimeout(t);
|
|
2444
|
+
}, [countdown, currentChallenge?.id, capturing, captureFrame, phase]);
|
|
2445
|
+
(0, import_react8.useEffect)(() => {
|
|
2446
|
+
if (session && !currentChallenge && completedChallenges > 0) {
|
|
2447
|
+
onComplete();
|
|
2448
|
+
}
|
|
2449
|
+
}, [session, currentChallenge, completedChallenges, onComplete]);
|
|
2450
|
+
if (cameraError) {
|
|
2451
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: styles.darkContainer, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: styles.errorContainer, children: [
|
|
2452
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { style: styles.errorText, children: cameraError }),
|
|
2453
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { style: styles.primaryButton, onClick: onCancel, children: "Go back" })
|
|
2454
|
+
] }) });
|
|
2455
|
+
}
|
|
2370
2456
|
if (!session) {
|
|
2371
2457
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: styles.darkContainer, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: styles.loadingContainer, children: [
|
|
2372
2458
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: styles.spinner }),
|
|
@@ -2381,34 +2467,136 @@ function LivenessScreen({
|
|
|
2381
2467
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h1", { style: styles.darkScreenTitle, children: "Liveness Check" }),
|
|
2382
2468
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { style: styles.glassCloseButton, onClick: onCancel, children: "\u2715" })
|
|
2383
2469
|
] }),
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2470
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
2471
|
+
"div",
|
|
2472
|
+
{
|
|
2473
|
+
style: {
|
|
2474
|
+
flex: 1,
|
|
2475
|
+
display: "flex",
|
|
2476
|
+
flexDirection: "column",
|
|
2477
|
+
alignItems: "center",
|
|
2478
|
+
justifyContent: "center",
|
|
2479
|
+
gap: "24px",
|
|
2480
|
+
padding: "16px 0"
|
|
2481
|
+
},
|
|
2482
|
+
children: [
|
|
2483
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { position: "relative" }, children: [
|
|
2484
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2485
|
+
"div",
|
|
2486
|
+
{
|
|
2487
|
+
style: {
|
|
2488
|
+
width: "240px",
|
|
2489
|
+
height: "300px",
|
|
2490
|
+
borderRadius: "50%",
|
|
2491
|
+
overflow: "hidden",
|
|
2492
|
+
backgroundColor: "#000",
|
|
2493
|
+
border: "3px solid rgba(255,255,255,0.2)"
|
|
2494
|
+
},
|
|
2495
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2496
|
+
"video",
|
|
2497
|
+
{
|
|
2498
|
+
ref: videoRef,
|
|
2499
|
+
autoPlay: true,
|
|
2500
|
+
playsInline: true,
|
|
2501
|
+
muted: true,
|
|
2502
|
+
style: {
|
|
2503
|
+
width: "100%",
|
|
2504
|
+
height: "100%",
|
|
2505
|
+
objectFit: "cover",
|
|
2506
|
+
transform: "scaleX(-1)"
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
)
|
|
2510
|
+
}
|
|
2511
|
+
),
|
|
2512
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2513
|
+
"svg",
|
|
2514
|
+
{
|
|
2515
|
+
style: {
|
|
2516
|
+
position: "absolute",
|
|
2517
|
+
top: "-8px",
|
|
2518
|
+
left: "-8px",
|
|
2519
|
+
pointerEvents: "none"
|
|
2520
|
+
},
|
|
2521
|
+
width: "256",
|
|
2522
|
+
height: "316",
|
|
2523
|
+
viewBox: "0 0 256 316",
|
|
2524
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2525
|
+
"ellipse",
|
|
2526
|
+
{
|
|
2527
|
+
cx: "128",
|
|
2528
|
+
cy: "158",
|
|
2529
|
+
rx: "124",
|
|
2530
|
+
ry: "154",
|
|
2531
|
+
fill: "none",
|
|
2532
|
+
stroke: phase === "capturing" ? colors.teal : "rgba(13,148,136,0.4)",
|
|
2533
|
+
strokeWidth: "5",
|
|
2534
|
+
strokeDasharray: `${completedChallenges / totalChallenges * 880} 880`,
|
|
2535
|
+
transform: "rotate(-90 128 158)",
|
|
2536
|
+
strokeLinecap: "round"
|
|
2537
|
+
}
|
|
2538
|
+
)
|
|
2539
|
+
}
|
|
2540
|
+
)
|
|
2541
|
+
] }),
|
|
2542
|
+
currentChallenge && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
2543
|
+
"div",
|
|
2395
2544
|
{
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2545
|
+
style: {
|
|
2546
|
+
textAlign: "center",
|
|
2547
|
+
padding: "20px 24px",
|
|
2548
|
+
borderRadius: "16px",
|
|
2549
|
+
backgroundColor: phase === "capturing" ? "rgba(13,148,136,0.18)" : "rgba(255,255,255,0.06)",
|
|
2550
|
+
border: phase === "capturing" ? `1px solid ${colors.teal}` : "1px solid rgba(255,255,255,0.08)",
|
|
2551
|
+
minWidth: "260px",
|
|
2552
|
+
transition: "background-color 200ms, border-color 200ms"
|
|
2553
|
+
},
|
|
2554
|
+
children: [
|
|
2555
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2556
|
+
"p",
|
|
2557
|
+
{
|
|
2558
|
+
style: {
|
|
2559
|
+
margin: 0,
|
|
2560
|
+
fontSize: "12px",
|
|
2561
|
+
fontWeight: 600,
|
|
2562
|
+
letterSpacing: "0.08em",
|
|
2563
|
+
textTransform: "uppercase",
|
|
2564
|
+
color: phase === "capturing" ? colors.teal : "rgba(255,255,255,0.5)"
|
|
2565
|
+
},
|
|
2566
|
+
children: capturing ? "Checking..." : phase === "preparing" ? "Get ready" : "Now \u2014 hold the pose"
|
|
2567
|
+
}
|
|
2568
|
+
),
|
|
2569
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2570
|
+
"h2",
|
|
2571
|
+
{
|
|
2572
|
+
style: {
|
|
2573
|
+
...styles.challengeTitle,
|
|
2574
|
+
margin: "8px 0 0",
|
|
2575
|
+
fontSize: "26px"
|
|
2576
|
+
},
|
|
2577
|
+
children: currentChallenge.instruction
|
|
2578
|
+
}
|
|
2579
|
+
),
|
|
2580
|
+
!capturing && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2581
|
+
"p",
|
|
2582
|
+
{
|
|
2583
|
+
style: {
|
|
2584
|
+
margin: "12px 0 0",
|
|
2585
|
+
fontSize: "32px",
|
|
2586
|
+
fontWeight: 700,
|
|
2587
|
+
color: phase === "capturing" ? colors.teal : "rgba(255,255,255,0.75)",
|
|
2588
|
+
lineHeight: 1
|
|
2589
|
+
},
|
|
2590
|
+
children: countdown
|
|
2591
|
+
}
|
|
2592
|
+
)
|
|
2593
|
+
]
|
|
2406
2594
|
}
|
|
2407
|
-
)
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2595
|
+
),
|
|
2596
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("canvas", { ref: canvasRef, style: { display: "none" } })
|
|
2597
|
+
]
|
|
2598
|
+
}
|
|
2599
|
+
),
|
|
2412
2600
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { padding: "16px 0" }, children: [
|
|
2413
2601
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: styles.progressDots, children: session.challenges.map((_, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2414
2602
|
"div",
|
|
@@ -2422,24 +2610,21 @@ function LivenessScreen({
|
|
|
2422
2610
|
)) }),
|
|
2423
2611
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("p", { style: styles.progressText, children: [
|
|
2424
2612
|
"Challenge ",
|
|
2425
|
-
completedChallenges + 1,
|
|
2426
|
-
" of
|
|
2613
|
+
Math.min(completedChallenges + 1, totalChallenges),
|
|
2614
|
+
" of",
|
|
2615
|
+
" ",
|
|
2427
2616
|
totalChallenges
|
|
2428
2617
|
] })
|
|
2429
2618
|
] }),
|
|
2430
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { padding: "
|
|
2431
|
-
"
|
|
2619
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { padding: "0 24px 24px", textAlign: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
2620
|
+
"p",
|
|
2432
2621
|
{
|
|
2433
|
-
style:
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
canvas.height = 100;
|
|
2438
|
-
canvas.toBlob(async (blob) => {
|
|
2439
|
-
if (blob) await onChallengeComplete(blob);
|
|
2440
|
-
});
|
|
2622
|
+
style: {
|
|
2623
|
+
color: "rgba(255,255,255,0.5)",
|
|
2624
|
+
fontSize: "13px",
|
|
2625
|
+
margin: 0
|
|
2441
2626
|
},
|
|
2442
|
-
children: "
|
|
2627
|
+
children: phase === "preparing" ? "Position your face inside the oval. Get ready for the next prompt." : "Hold the pose until the capture completes."
|
|
2443
2628
|
}
|
|
2444
2629
|
) })
|
|
2445
2630
|
] });
|
package/dist/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ function useKoraIDVContext() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// src/hooks/useKoraIDV.ts
|
|
37
|
-
import { useState, useCallback } from "react";
|
|
37
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
38
38
|
import {
|
|
39
39
|
KoraError
|
|
40
40
|
} from "@koraidv/core";
|
|
@@ -281,6 +281,15 @@ function useKoraIDV() {
|
|
|
281
281
|
return null;
|
|
282
282
|
}
|
|
283
283
|
}, [sdk]);
|
|
284
|
+
const completionFiredRef = useRef(false);
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (state.step === "processing" && !completionFiredRef.current) {
|
|
287
|
+
completionFiredRef.current = true;
|
|
288
|
+
complete();
|
|
289
|
+
} else if (state.step !== "processing" && state.step !== "complete") {
|
|
290
|
+
completionFiredRef.current = false;
|
|
291
|
+
}
|
|
292
|
+
}, [state.step, complete]);
|
|
284
293
|
const cancel = useCallback(() => {
|
|
285
294
|
sdk.reset();
|
|
286
295
|
setState({
|
|
@@ -319,7 +328,7 @@ function useKoraIDV() {
|
|
|
319
328
|
}
|
|
320
329
|
|
|
321
330
|
// src/components/VerificationFlow.tsx
|
|
322
|
-
import { useEffect as
|
|
331
|
+
import { useEffect as useEffect8, useState as useState6 } from "react";
|
|
323
332
|
import { KoraError as KoraError2, KoraErrorCode } from "@koraidv/core";
|
|
324
333
|
|
|
325
334
|
// src/components/styles.ts
|
|
@@ -1299,7 +1308,7 @@ var styles = {
|
|
|
1299
1308
|
};
|
|
1300
1309
|
|
|
1301
1310
|
// src/components/DesignSystem.tsx
|
|
1302
|
-
import { useEffect } from "react";
|
|
1311
|
+
import { useEffect as useEffect2 } from "react";
|
|
1303
1312
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1304
1313
|
function StepProgressBar({ total, current, isDark = false }) {
|
|
1305
1314
|
return /* @__PURE__ */ jsx2("div", { style: styles.progressBar, children: Array.from({ length: total }).map((_, i) => /* @__PURE__ */ jsx2(
|
|
@@ -1373,7 +1382,7 @@ function ScoreMetricRow({ label, score, icon, status, message }) {
|
|
|
1373
1382
|
);
|
|
1374
1383
|
}
|
|
1375
1384
|
function ProcessingScreen({ steps }) {
|
|
1376
|
-
|
|
1385
|
+
useEffect2(() => {
|
|
1377
1386
|
injectKeyframes();
|
|
1378
1387
|
}, []);
|
|
1379
1388
|
return /* @__PURE__ */ jsxs("div", { style: styles.processingContainer, children: [
|
|
@@ -1743,7 +1752,7 @@ function getIcon(type) {
|
|
|
1743
1752
|
}
|
|
1744
1753
|
|
|
1745
1754
|
// src/components/DocumentCaptureScreen.tsx
|
|
1746
|
-
import { useRef, useEffect as
|
|
1755
|
+
import { useRef as useRef2, useEffect as useEffect3, useState as useState3, useCallback as useCallback2 } from "react";
|
|
1747
1756
|
import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1748
1757
|
var qualityIssueMessages = {
|
|
1749
1758
|
face_blurred: "Photo on document is blurry. Retake in better lighting.",
|
|
@@ -1763,9 +1772,9 @@ function DocumentCaptureScreen({
|
|
|
1763
1772
|
onCapture,
|
|
1764
1773
|
onCancel
|
|
1765
1774
|
}) {
|
|
1766
|
-
const videoRef =
|
|
1767
|
-
const canvasRef =
|
|
1768
|
-
const guideRef =
|
|
1775
|
+
const videoRef = useRef2(null);
|
|
1776
|
+
const canvasRef = useRef2(null);
|
|
1777
|
+
const guideRef = useRef2(null);
|
|
1769
1778
|
const [stream, setStream] = useState3(null);
|
|
1770
1779
|
const [isCapturing, setIsCapturing] = useState3(false);
|
|
1771
1780
|
const [error, setError] = useState3(null);
|
|
@@ -1774,10 +1783,10 @@ function DocumentCaptureScreen({
|
|
|
1774
1783
|
const [qualityResult, setQualityResult] = useState3(null);
|
|
1775
1784
|
const [isCheckingQuality, setIsCheckingQuality] = useState3(false);
|
|
1776
1785
|
const [retakeCount, setRetakeCount] = useState3(0);
|
|
1777
|
-
|
|
1786
|
+
useEffect3(() => {
|
|
1778
1787
|
injectKeyframes();
|
|
1779
1788
|
}, []);
|
|
1780
|
-
|
|
1789
|
+
useEffect3(() => {
|
|
1781
1790
|
let mounted = true;
|
|
1782
1791
|
async function startCamera() {
|
|
1783
1792
|
try {
|
|
@@ -1799,7 +1808,7 @@ function DocumentCaptureScreen({
|
|
|
1799
1808
|
mounted = false;
|
|
1800
1809
|
};
|
|
1801
1810
|
}, [capturedImage]);
|
|
1802
|
-
|
|
1811
|
+
useEffect3(() => {
|
|
1803
1812
|
return () => {
|
|
1804
1813
|
stream?.getTracks().forEach((t) => t.stop());
|
|
1805
1814
|
};
|
|
@@ -2042,10 +2051,10 @@ function QualityCheck({ label }) {
|
|
|
2042
2051
|
}
|
|
2043
2052
|
|
|
2044
2053
|
// src/components/FlipDocumentScreen.tsx
|
|
2045
|
-
import { useEffect as
|
|
2054
|
+
import { useEffect as useEffect4 } from "react";
|
|
2046
2055
|
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2047
2056
|
function FlipDocumentScreen({ onContinue, onCancel }) {
|
|
2048
|
-
|
|
2057
|
+
useEffect4(() => {
|
|
2049
2058
|
injectKeyframes();
|
|
2050
2059
|
}, []);
|
|
2051
2060
|
return /* @__PURE__ */ jsxs6("div", { style: styles.darkContainer, children: [
|
|
@@ -2109,20 +2118,20 @@ function FlipDocumentScreen({ onContinue, onCancel }) {
|
|
|
2109
2118
|
}
|
|
2110
2119
|
|
|
2111
2120
|
// src/components/SelfieCaptureScreen.tsx
|
|
2112
|
-
import { useRef as
|
|
2121
|
+
import { useRef as useRef3, useEffect as useEffect5, useState as useState4, useCallback as useCallback3 } from "react";
|
|
2113
2122
|
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2114
2123
|
function SelfieCaptureScreen({ onCapture, onCancel }) {
|
|
2115
|
-
const videoRef =
|
|
2116
|
-
const canvasRef =
|
|
2124
|
+
const videoRef = useRef3(null);
|
|
2125
|
+
const canvasRef = useRef3(null);
|
|
2117
2126
|
const [stream, setStream] = useState4(null);
|
|
2118
2127
|
const [isCapturing, setIsCapturing] = useState4(false);
|
|
2119
2128
|
const [error, setError] = useState4(null);
|
|
2120
2129
|
const [capturedImage, setCapturedImage] = useState4(null);
|
|
2121
2130
|
const [capturedBlob, setCapturedBlob] = useState4(null);
|
|
2122
|
-
|
|
2131
|
+
useEffect5(() => {
|
|
2123
2132
|
injectKeyframes();
|
|
2124
2133
|
}, []);
|
|
2125
|
-
|
|
2134
|
+
useEffect5(() => {
|
|
2126
2135
|
let mounted = true;
|
|
2127
2136
|
async function startCamera() {
|
|
2128
2137
|
try {
|
|
@@ -2144,7 +2153,7 @@ function SelfieCaptureScreen({ onCapture, onCancel }) {
|
|
|
2144
2153
|
mounted = false;
|
|
2145
2154
|
};
|
|
2146
2155
|
}, [capturedImage]);
|
|
2147
|
-
|
|
2156
|
+
useEffect5(() => {
|
|
2148
2157
|
return () => {
|
|
2149
2158
|
stream?.getTracks().forEach((t) => t.stop());
|
|
2150
2159
|
};
|
|
@@ -2281,7 +2290,7 @@ function QualityCheck2({ label }) {
|
|
|
2281
2290
|
}
|
|
2282
2291
|
|
|
2283
2292
|
// src/components/LivenessScreen.tsx
|
|
2284
|
-
import { useEffect as
|
|
2293
|
+
import { useEffect as useEffect6, useRef as useRef4, useState as useState5, useCallback as useCallback4 } from "react";
|
|
2285
2294
|
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2286
2295
|
function LivenessScreen({
|
|
2287
2296
|
session,
|
|
@@ -2292,32 +2301,109 @@ function LivenessScreen({
|
|
|
2292
2301
|
onComplete,
|
|
2293
2302
|
onCancel
|
|
2294
2303
|
}) {
|
|
2304
|
+
const videoRef = useRef4(null);
|
|
2305
|
+
const canvasRef = useRef4(null);
|
|
2306
|
+
const [stream, setStream] = useState5(null);
|
|
2307
|
+
const [cameraError, setCameraError] = useState5(null);
|
|
2308
|
+
const [phase, setPhase] = useState5("preparing");
|
|
2295
2309
|
const [countdown, setCountdown] = useState5(3);
|
|
2296
|
-
|
|
2310
|
+
const [capturing, setCapturing] = useState5(false);
|
|
2311
|
+
useEffect6(() => {
|
|
2297
2312
|
injectKeyframes();
|
|
2298
2313
|
}, []);
|
|
2299
|
-
|
|
2314
|
+
useEffect6(() => {
|
|
2300
2315
|
if (!session) onStart();
|
|
2301
2316
|
}, [session, onStart]);
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2317
|
+
useEffect6(() => {
|
|
2318
|
+
let mounted = true;
|
|
2319
|
+
async function startCamera() {
|
|
2320
|
+
try {
|
|
2321
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
2322
|
+
video: {
|
|
2323
|
+
facingMode: "user",
|
|
2324
|
+
width: { ideal: 720 },
|
|
2325
|
+
height: { ideal: 720 }
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
if (!mounted) {
|
|
2329
|
+
mediaStream.getTracks().forEach((t) => t.stop());
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
setStream(mediaStream);
|
|
2333
|
+
if (videoRef.current) {
|
|
2334
|
+
videoRef.current.srcObject = mediaStream;
|
|
2335
|
+
}
|
|
2336
|
+
} catch {
|
|
2337
|
+
if (mounted) {
|
|
2338
|
+
setCameraError(
|
|
2339
|
+
"Camera access denied. Please enable camera permissions and try again."
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2305
2343
|
}
|
|
2306
|
-
|
|
2307
|
-
|
|
2344
|
+
startCamera();
|
|
2345
|
+
return () => {
|
|
2346
|
+
mounted = false;
|
|
2347
|
+
};
|
|
2348
|
+
}, []);
|
|
2349
|
+
useEffect6(() => {
|
|
2350
|
+
return () => {
|
|
2351
|
+
stream?.getTracks().forEach((t) => t.stop());
|
|
2352
|
+
};
|
|
2353
|
+
}, [stream]);
|
|
2354
|
+
useEffect6(() => {
|
|
2308
2355
|
if (!currentChallenge) return;
|
|
2356
|
+
setPhase("preparing");
|
|
2309
2357
|
setCountdown(3);
|
|
2310
|
-
const interval = setInterval(() => {
|
|
2311
|
-
setCountdown((c) => {
|
|
2312
|
-
if (c <= 1) {
|
|
2313
|
-
clearInterval(interval);
|
|
2314
|
-
return 0;
|
|
2315
|
-
}
|
|
2316
|
-
return c - 1;
|
|
2317
|
-
});
|
|
2318
|
-
}, 1e3);
|
|
2319
|
-
return () => clearInterval(interval);
|
|
2320
2358
|
}, [currentChallenge?.id]);
|
|
2359
|
+
const captureFrame = useCallback4(async () => {
|
|
2360
|
+
if (!currentChallenge || !videoRef.current || !canvasRef.current || capturing) {
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
const video = videoRef.current;
|
|
2364
|
+
const canvas = canvasRef.current;
|
|
2365
|
+
const ctx = canvas.getContext("2d");
|
|
2366
|
+
if (!ctx || video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
2367
|
+
setCapturing(true);
|
|
2368
|
+
canvas.width = video.videoWidth;
|
|
2369
|
+
canvas.height = video.videoHeight;
|
|
2370
|
+
ctx.drawImage(video, 0, 0);
|
|
2371
|
+
canvas.toBlob(
|
|
2372
|
+
async (blob) => {
|
|
2373
|
+
if (blob) {
|
|
2374
|
+
await onChallengeComplete(blob);
|
|
2375
|
+
}
|
|
2376
|
+
setCapturing(false);
|
|
2377
|
+
},
|
|
2378
|
+
"image/jpeg",
|
|
2379
|
+
0.85
|
|
2380
|
+
);
|
|
2381
|
+
}, [currentChallenge, capturing, onChallengeComplete]);
|
|
2382
|
+
useEffect6(() => {
|
|
2383
|
+
if (!currentChallenge || capturing) return;
|
|
2384
|
+
if (countdown === 0) {
|
|
2385
|
+
if (phase === "preparing") {
|
|
2386
|
+
setPhase("capturing");
|
|
2387
|
+
setCountdown(3);
|
|
2388
|
+
} else {
|
|
2389
|
+
captureFrame();
|
|
2390
|
+
}
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const t = setTimeout(() => setCountdown((c) => c - 1), 1e3);
|
|
2394
|
+
return () => clearTimeout(t);
|
|
2395
|
+
}, [countdown, currentChallenge?.id, capturing, captureFrame, phase]);
|
|
2396
|
+
useEffect6(() => {
|
|
2397
|
+
if (session && !currentChallenge && completedChallenges > 0) {
|
|
2398
|
+
onComplete();
|
|
2399
|
+
}
|
|
2400
|
+
}, [session, currentChallenge, completedChallenges, onComplete]);
|
|
2401
|
+
if (cameraError) {
|
|
2402
|
+
return /* @__PURE__ */ jsx9("div", { style: styles.darkContainer, children: /* @__PURE__ */ jsxs8("div", { style: styles.errorContainer, children: [
|
|
2403
|
+
/* @__PURE__ */ jsx9("p", { style: styles.errorText, children: cameraError }),
|
|
2404
|
+
/* @__PURE__ */ jsx9("button", { style: styles.primaryButton, onClick: onCancel, children: "Go back" })
|
|
2405
|
+
] }) });
|
|
2406
|
+
}
|
|
2321
2407
|
if (!session) {
|
|
2322
2408
|
return /* @__PURE__ */ jsx9("div", { style: styles.darkContainer, children: /* @__PURE__ */ jsxs8("div", { style: styles.loadingContainer, children: [
|
|
2323
2409
|
/* @__PURE__ */ jsx9("div", { style: styles.spinner }),
|
|
@@ -2332,34 +2418,136 @@ function LivenessScreen({
|
|
|
2332
2418
|
/* @__PURE__ */ jsx9("h1", { style: styles.darkScreenTitle, children: "Liveness Check" }),
|
|
2333
2419
|
/* @__PURE__ */ jsx9("button", { style: styles.glassCloseButton, onClick: onCancel, children: "\u2715" })
|
|
2334
2420
|
] }),
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2421
|
+
/* @__PURE__ */ jsxs8(
|
|
2422
|
+
"div",
|
|
2423
|
+
{
|
|
2424
|
+
style: {
|
|
2425
|
+
flex: 1,
|
|
2426
|
+
display: "flex",
|
|
2427
|
+
flexDirection: "column",
|
|
2428
|
+
alignItems: "center",
|
|
2429
|
+
justifyContent: "center",
|
|
2430
|
+
gap: "24px",
|
|
2431
|
+
padding: "16px 0"
|
|
2432
|
+
},
|
|
2433
|
+
children: [
|
|
2434
|
+
/* @__PURE__ */ jsxs8("div", { style: { position: "relative" }, children: [
|
|
2435
|
+
/* @__PURE__ */ jsx9(
|
|
2436
|
+
"div",
|
|
2437
|
+
{
|
|
2438
|
+
style: {
|
|
2439
|
+
width: "240px",
|
|
2440
|
+
height: "300px",
|
|
2441
|
+
borderRadius: "50%",
|
|
2442
|
+
overflow: "hidden",
|
|
2443
|
+
backgroundColor: "#000",
|
|
2444
|
+
border: "3px solid rgba(255,255,255,0.2)"
|
|
2445
|
+
},
|
|
2446
|
+
children: /* @__PURE__ */ jsx9(
|
|
2447
|
+
"video",
|
|
2448
|
+
{
|
|
2449
|
+
ref: videoRef,
|
|
2450
|
+
autoPlay: true,
|
|
2451
|
+
playsInline: true,
|
|
2452
|
+
muted: true,
|
|
2453
|
+
style: {
|
|
2454
|
+
width: "100%",
|
|
2455
|
+
height: "100%",
|
|
2456
|
+
objectFit: "cover",
|
|
2457
|
+
transform: "scaleX(-1)"
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
)
|
|
2461
|
+
}
|
|
2462
|
+
),
|
|
2463
|
+
/* @__PURE__ */ jsx9(
|
|
2464
|
+
"svg",
|
|
2465
|
+
{
|
|
2466
|
+
style: {
|
|
2467
|
+
position: "absolute",
|
|
2468
|
+
top: "-8px",
|
|
2469
|
+
left: "-8px",
|
|
2470
|
+
pointerEvents: "none"
|
|
2471
|
+
},
|
|
2472
|
+
width: "256",
|
|
2473
|
+
height: "316",
|
|
2474
|
+
viewBox: "0 0 256 316",
|
|
2475
|
+
children: /* @__PURE__ */ jsx9(
|
|
2476
|
+
"ellipse",
|
|
2477
|
+
{
|
|
2478
|
+
cx: "128",
|
|
2479
|
+
cy: "158",
|
|
2480
|
+
rx: "124",
|
|
2481
|
+
ry: "154",
|
|
2482
|
+
fill: "none",
|
|
2483
|
+
stroke: phase === "capturing" ? colors.teal : "rgba(13,148,136,0.4)",
|
|
2484
|
+
strokeWidth: "5",
|
|
2485
|
+
strokeDasharray: `${completedChallenges / totalChallenges * 880} 880`,
|
|
2486
|
+
transform: "rotate(-90 128 158)",
|
|
2487
|
+
strokeLinecap: "round"
|
|
2488
|
+
}
|
|
2489
|
+
)
|
|
2490
|
+
}
|
|
2491
|
+
)
|
|
2492
|
+
] }),
|
|
2493
|
+
currentChallenge && /* @__PURE__ */ jsxs8(
|
|
2494
|
+
"div",
|
|
2346
2495
|
{
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2496
|
+
style: {
|
|
2497
|
+
textAlign: "center",
|
|
2498
|
+
padding: "20px 24px",
|
|
2499
|
+
borderRadius: "16px",
|
|
2500
|
+
backgroundColor: phase === "capturing" ? "rgba(13,148,136,0.18)" : "rgba(255,255,255,0.06)",
|
|
2501
|
+
border: phase === "capturing" ? `1px solid ${colors.teal}` : "1px solid rgba(255,255,255,0.08)",
|
|
2502
|
+
minWidth: "260px",
|
|
2503
|
+
transition: "background-color 200ms, border-color 200ms"
|
|
2504
|
+
},
|
|
2505
|
+
children: [
|
|
2506
|
+
/* @__PURE__ */ jsx9(
|
|
2507
|
+
"p",
|
|
2508
|
+
{
|
|
2509
|
+
style: {
|
|
2510
|
+
margin: 0,
|
|
2511
|
+
fontSize: "12px",
|
|
2512
|
+
fontWeight: 600,
|
|
2513
|
+
letterSpacing: "0.08em",
|
|
2514
|
+
textTransform: "uppercase",
|
|
2515
|
+
color: phase === "capturing" ? colors.teal : "rgba(255,255,255,0.5)"
|
|
2516
|
+
},
|
|
2517
|
+
children: capturing ? "Checking..." : phase === "preparing" ? "Get ready" : "Now \u2014 hold the pose"
|
|
2518
|
+
}
|
|
2519
|
+
),
|
|
2520
|
+
/* @__PURE__ */ jsx9(
|
|
2521
|
+
"h2",
|
|
2522
|
+
{
|
|
2523
|
+
style: {
|
|
2524
|
+
...styles.challengeTitle,
|
|
2525
|
+
margin: "8px 0 0",
|
|
2526
|
+
fontSize: "26px"
|
|
2527
|
+
},
|
|
2528
|
+
children: currentChallenge.instruction
|
|
2529
|
+
}
|
|
2530
|
+
),
|
|
2531
|
+
!capturing && /* @__PURE__ */ jsx9(
|
|
2532
|
+
"p",
|
|
2533
|
+
{
|
|
2534
|
+
style: {
|
|
2535
|
+
margin: "12px 0 0",
|
|
2536
|
+
fontSize: "32px",
|
|
2537
|
+
fontWeight: 700,
|
|
2538
|
+
color: phase === "capturing" ? colors.teal : "rgba(255,255,255,0.75)",
|
|
2539
|
+
lineHeight: 1
|
|
2540
|
+
},
|
|
2541
|
+
children: countdown
|
|
2542
|
+
}
|
|
2543
|
+
)
|
|
2544
|
+
]
|
|
2357
2545
|
}
|
|
2358
|
-
)
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2546
|
+
),
|
|
2547
|
+
/* @__PURE__ */ jsx9("canvas", { ref: canvasRef, style: { display: "none" } })
|
|
2548
|
+
]
|
|
2549
|
+
}
|
|
2550
|
+
),
|
|
2363
2551
|
/* @__PURE__ */ jsxs8("div", { style: { padding: "16px 0" }, children: [
|
|
2364
2552
|
/* @__PURE__ */ jsx9("div", { style: styles.progressDots, children: session.challenges.map((_, index) => /* @__PURE__ */ jsx9(
|
|
2365
2553
|
"div",
|
|
@@ -2373,24 +2561,21 @@ function LivenessScreen({
|
|
|
2373
2561
|
)) }),
|
|
2374
2562
|
/* @__PURE__ */ jsxs8("p", { style: styles.progressText, children: [
|
|
2375
2563
|
"Challenge ",
|
|
2376
|
-
completedChallenges + 1,
|
|
2377
|
-
" of
|
|
2564
|
+
Math.min(completedChallenges + 1, totalChallenges),
|
|
2565
|
+
" of",
|
|
2566
|
+
" ",
|
|
2378
2567
|
totalChallenges
|
|
2379
2568
|
] })
|
|
2380
2569
|
] }),
|
|
2381
|
-
/* @__PURE__ */ jsx9("div", { style: { padding: "
|
|
2382
|
-
"
|
|
2570
|
+
/* @__PURE__ */ jsx9("div", { style: { padding: "0 24px 24px", textAlign: "center" }, children: /* @__PURE__ */ jsx9(
|
|
2571
|
+
"p",
|
|
2383
2572
|
{
|
|
2384
|
-
style:
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
canvas.height = 100;
|
|
2389
|
-
canvas.toBlob(async (blob) => {
|
|
2390
|
-
if (blob) await onChallengeComplete(blob);
|
|
2391
|
-
});
|
|
2573
|
+
style: {
|
|
2574
|
+
color: "rgba(255,255,255,0.5)",
|
|
2575
|
+
fontSize: "13px",
|
|
2576
|
+
margin: 0
|
|
2392
2577
|
},
|
|
2393
|
-
children: "
|
|
2578
|
+
children: phase === "preparing" ? "Position your face inside the oval. Get ready for the next prompt." : "Hold the pose until the capture completes."
|
|
2394
2579
|
}
|
|
2395
2580
|
) })
|
|
2396
2581
|
] });
|
|
@@ -2763,10 +2948,10 @@ function ErrorScreen({ error, onRetry, onCancel }) {
|
|
|
2763
2948
|
}
|
|
2764
2949
|
|
|
2765
2950
|
// src/components/LoadingScreen.tsx
|
|
2766
|
-
import { useEffect as
|
|
2951
|
+
import { useEffect as useEffect7 } from "react";
|
|
2767
2952
|
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2768
2953
|
function LoadingScreen({ message = "Loading..." }) {
|
|
2769
|
-
|
|
2954
|
+
useEffect7(() => {
|
|
2770
2955
|
injectKeyframes();
|
|
2771
2956
|
}, []);
|
|
2772
2957
|
return /* @__PURE__ */ jsx12("div", { style: styles.container, children: /* @__PURE__ */ jsxs11("div", { style: styles.loadingContainer, children: [
|
|
@@ -2823,20 +3008,20 @@ function VerificationFlow({
|
|
|
2823
3008
|
const [showFlipInstruction, setShowFlipInstruction] = useState6(true);
|
|
2824
3009
|
const [supportedCountries, setSupportedCountries] = useState6([]);
|
|
2825
3010
|
const [countriesLoading, setCountriesLoading] = useState6(false);
|
|
2826
|
-
|
|
3011
|
+
useEffect8(() => {
|
|
2827
3012
|
if (state.step === "document_front") {
|
|
2828
3013
|
setShowFlipInstruction(true);
|
|
2829
3014
|
}
|
|
2830
3015
|
}, [state.step]);
|
|
2831
|
-
|
|
3016
|
+
useEffect8(() => {
|
|
2832
3017
|
startVerification(externalId, tier);
|
|
2833
3018
|
}, [externalId, tier, startVerification]);
|
|
2834
|
-
|
|
3019
|
+
useEffect8(() => {
|
|
2835
3020
|
if (state.step === "complete" && state.verification && onComplete) {
|
|
2836
3021
|
onComplete(state.verification);
|
|
2837
3022
|
}
|
|
2838
3023
|
}, [state.step, state.verification, onComplete]);
|
|
2839
|
-
|
|
3024
|
+
useEffect8(() => {
|
|
2840
3025
|
if (state.error && onError) {
|
|
2841
3026
|
onError(state.error);
|
|
2842
3027
|
}
|
|
@@ -2972,7 +3157,7 @@ function VerificationFlow({
|
|
|
2972
3157
|
}
|
|
2973
3158
|
|
|
2974
3159
|
// src/components/QrHandoffScreen.tsx
|
|
2975
|
-
import { useEffect as
|
|
3160
|
+
import { useEffect as useEffect9, useState as useState7, useRef as useRef5 } from "react";
|
|
2976
3161
|
import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2977
3162
|
function QrHandoffScreen({
|
|
2978
3163
|
session,
|
|
@@ -2985,8 +3170,8 @@ function QrHandoffScreen({
|
|
|
2985
3170
|
const [timeLeft, setTimeLeft] = useState7(session.expiresIn);
|
|
2986
3171
|
const [scanned, setScanned] = useState7(false);
|
|
2987
3172
|
const [expired, setExpired] = useState7(false);
|
|
2988
|
-
const timerRef =
|
|
2989
|
-
|
|
3173
|
+
const timerRef = useRef5();
|
|
3174
|
+
useEffect9(() => {
|
|
2990
3175
|
setTimeLeft(session.expiresIn);
|
|
2991
3176
|
setExpired(false);
|
|
2992
3177
|
setScanned(false);
|
|
@@ -3003,7 +3188,7 @@ function QrHandoffScreen({
|
|
|
3003
3188
|
}, 1e3);
|
|
3004
3189
|
return () => clearInterval(timerRef.current);
|
|
3005
3190
|
}, [session.token]);
|
|
3006
|
-
|
|
3191
|
+
useEffect9(() => {
|
|
3007
3192
|
if (!eventSource) return;
|
|
3008
3193
|
const handleStatus = (event) => {
|
|
3009
3194
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koraidv/react",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
4
4
|
"description": "Kora IDV React Components for Identity Verification",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@koraidv/core": "^1.7.
|
|
24
|
+
"@koraidv/core": "^1.7.8"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/react": "^18.2.0",
|