astra-sdk-web 1.1.7 → 1.1.9
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/astra-sdk.cjs.js +90 -20
- package/dist/astra-sdk.cjs.js.map +1 -1
- package/dist/astra-sdk.css +16 -0
- package/dist/astra-sdk.css.map +1 -1
- package/dist/astra-sdk.d.cts +0 -4
- package/dist/astra-sdk.es.js +90 -20
- package/dist/astra-sdk.es.js.map +1 -1
- package/dist/components.cjs.js +90 -20
- package/dist/components.cjs.js.map +1 -1
- package/dist/components.css +16 -0
- package/dist/components.css.map +1 -1
- package/dist/components.es.js +90 -20
- package/dist/components.es.js.map +1 -1
- package/dist/index.d.ts +0 -4
- package/package.json +1 -1
- package/src/features/faceScan/hooks/useFaceScan.ts +24 -0
- package/src/pages/DocumentUploadModal.tsx +21 -1
- package/src/pages/FaceScanModal.tsx +26 -14
- package/src/services/faceMeshService.ts +47 -15
- package/src/services/kycApiService.ts +7 -0
package/dist/index.d.ts
CHANGED
|
@@ -76,10 +76,6 @@ declare class ApiClient {
|
|
|
76
76
|
getConfig(): Required<AstraSDKConfig>;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
* KYC API Service
|
|
81
|
-
* Handles all KYC-related API calls (face scan, document upload, status check)
|
|
82
|
-
*/
|
|
83
79
|
interface KycApiConfig {
|
|
84
80
|
apiBaseUrl: string;
|
|
85
81
|
sessionId: string;
|
package/package.json
CHANGED
|
@@ -49,6 +49,9 @@ export function useFaceScan(
|
|
|
49
49
|
lastResultsAt: 0,
|
|
50
50
|
stage: 'CENTER' as LivenessStage,
|
|
51
51
|
livenessReady: false,
|
|
52
|
+
currentYaw: null as number | null,
|
|
53
|
+
currentAbsYaw: null as number | null,
|
|
54
|
+
livenessCompleted: false,
|
|
52
55
|
});
|
|
53
56
|
|
|
54
57
|
// Sync refs with state
|
|
@@ -74,6 +77,27 @@ export function useFaceScan(
|
|
|
74
77
|
|
|
75
78
|
const handleFaceCapture = useCallback(async () => {
|
|
76
79
|
if (!videoRef.current) return;
|
|
80
|
+
|
|
81
|
+
// Check if face is straight before capturing (reduced threshold, no center check)
|
|
82
|
+
const reducedThreshold = 0.08; // More lenient threshold - only check if face is straight
|
|
83
|
+
const currentAbsYaw = livenessStateRef.current.currentAbsYaw;
|
|
84
|
+
|
|
85
|
+
if (currentAbsYaw === null || currentAbsYaw === undefined) {
|
|
86
|
+
setState(prev => ({
|
|
87
|
+
...prev,
|
|
88
|
+
livenessInstruction: "Please position your face in front of the camera",
|
|
89
|
+
}));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (currentAbsYaw >= reducedThreshold) {
|
|
94
|
+
setState(prev => ({
|
|
95
|
+
...prev,
|
|
96
|
+
livenessInstruction: "Please look straight at the camera before capturing",
|
|
97
|
+
}));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
77
101
|
setState(prev => ({ ...prev, loading: true }));
|
|
78
102
|
try {
|
|
79
103
|
const video = videoRef.current;
|
|
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import { useNavigate } from 'react-router-dom';
|
|
3
3
|
import { useDocumentUpload } from '../features/documentUpload/hooks/useDocumentUpload';
|
|
4
4
|
import { useKycContext } from '../contexts/KycContext';
|
|
5
|
+
import { COMPLETED_STEPS } from '../services/kycApiService';
|
|
5
6
|
import type { DocumentType } from '../features/documentUpload/types';
|
|
6
7
|
|
|
7
8
|
interface DocumentUploadModalProps {
|
|
@@ -47,7 +48,26 @@ function DocumentUploadModal({ onComplete }: DocumentUploadModalProps) {
|
|
|
47
48
|
if (!apiService) return;
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
|
-
await apiService.
|
|
51
|
+
const statusResponse = await apiService.getSessionStatus();
|
|
52
|
+
const { completed_steps, next_step, status } = statusResponse.data;
|
|
53
|
+
|
|
54
|
+
// Check if session is active
|
|
55
|
+
if (status !== 'ACTIVE') {
|
|
56
|
+
throw new Error('Session expired or inactive');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If document_upload is already completed, show completion message
|
|
60
|
+
if (completed_steps.includes(COMPLETED_STEPS.DOCS)) {
|
|
61
|
+
// Document already uploaded, could show completion or redirect
|
|
62
|
+
console.log('Document already uploaded');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If next_step is not document_upload and face_scan is not completed, redirect to face scan
|
|
66
|
+
if (next_step === COMPLETED_STEPS.FACE && !completed_steps.includes(COMPLETED_STEPS.FACE)) {
|
|
67
|
+
// Should not happen if we're in document upload modal, but handle it
|
|
68
|
+
console.warn('Face scan not completed, but in document upload modal');
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
setSessionError(null);
|
|
52
72
|
} catch (error: any) {
|
|
53
73
|
const message = error.message || 'Session expired or inactive';
|
|
@@ -5,6 +5,7 @@ import { useCamera } from '../features/faceScan/hooks/useCamera';
|
|
|
5
5
|
import { useFaceScan } from '../features/faceScan/hooks/useFaceScan';
|
|
6
6
|
import { useKycContext } from '../contexts/KycContext';
|
|
7
7
|
import { Toast } from '../components/Toast';
|
|
8
|
+
import { COMPLETED_STEPS } from '../services/kycApiService';
|
|
8
9
|
import '../index.css';
|
|
9
10
|
|
|
10
11
|
interface FaceScanModalProps {
|
|
@@ -28,7 +29,6 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
28
29
|
try {
|
|
29
30
|
await apiService.uploadFaceScan(blob);
|
|
30
31
|
} catch (error: any) {
|
|
31
|
-
// Check if it's a "Face already registered" error
|
|
32
32
|
const errorMessage = error?.message || '';
|
|
33
33
|
const errorData = (error as any)?.errorData || {};
|
|
34
34
|
|
|
@@ -39,13 +39,11 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
39
39
|
(error as any)?.statusCode === 500 && errorMessage.includes('Face')
|
|
40
40
|
) {
|
|
41
41
|
setToast({
|
|
42
|
-
message: 'Face
|
|
42
|
+
message: 'Face already registered',
|
|
43
43
|
type: 'warning',
|
|
44
44
|
});
|
|
45
|
-
// Don't throw error - allow flow to continue to document upload
|
|
46
45
|
return;
|
|
47
46
|
}
|
|
48
|
-
// Re-throw other errors
|
|
49
47
|
throw error;
|
|
50
48
|
}
|
|
51
49
|
},
|
|
@@ -56,18 +54,37 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
56
54
|
},
|
|
57
55
|
});
|
|
58
56
|
|
|
59
|
-
// Check session status on mount
|
|
60
57
|
useEffect(() => {
|
|
61
58
|
const checkSession = async () => {
|
|
62
59
|
if (!apiService) return;
|
|
63
60
|
|
|
64
61
|
try {
|
|
65
|
-
await apiService.
|
|
62
|
+
const statusResponse = await apiService.getSessionStatus();
|
|
63
|
+
const { completed_steps, next_step, status } = statusResponse.data;
|
|
64
|
+
|
|
65
|
+
// Check if session is active
|
|
66
|
+
if (status !== 'ACTIVE') {
|
|
67
|
+
throw new Error('Session expired or inactive');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If face_scan is already completed, skip to document upload
|
|
71
|
+
if (completed_steps.includes(COMPLETED_STEPS.FACE)) {
|
|
72
|
+
setState(prev => ({ ...prev, showDocumentUpload: true }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If next_step is not face_scan, redirect accordingly
|
|
77
|
+
if (next_step !== COMPLETED_STEPS.FACE && next_step !== COMPLETED_STEPS.INITIATED) {
|
|
78
|
+
if (next_step === COMPLETED_STEPS.DOCS) {
|
|
79
|
+
setState(prev => ({ ...prev, showDocumentUpload: true }));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
66
84
|
setSessionError(null);
|
|
67
85
|
} catch (error: any) {
|
|
68
86
|
const message = error.message || 'Session expired or inactive';
|
|
69
87
|
setSessionError(message);
|
|
70
|
-
// Redirect to QR page after showing error
|
|
71
88
|
setTimeout(() => {
|
|
72
89
|
navigate('/qr', { replace: true });
|
|
73
90
|
}, 2000);
|
|
@@ -75,17 +92,14 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
75
92
|
};
|
|
76
93
|
|
|
77
94
|
checkSession();
|
|
78
|
-
}, [apiService, navigate]);
|
|
95
|
+
}, [apiService, navigate, setState]);
|
|
79
96
|
|
|
80
|
-
// Sync camera ready state
|
|
81
97
|
useEffect(() => {
|
|
82
98
|
setState(prev => ({ ...prev, cameraReady }));
|
|
83
99
|
}, [cameraReady, setState]);
|
|
84
100
|
|
|
85
|
-
// Log session info and call status API when camera opens
|
|
86
101
|
useEffect(() => {
|
|
87
102
|
if (cameraReady && apiService) {
|
|
88
|
-
// Get config from apiService to log session info
|
|
89
103
|
const config = apiService.getConfig();
|
|
90
104
|
if (config) {
|
|
91
105
|
console.log('=== Camera Opened ===');
|
|
@@ -94,7 +108,6 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
94
108
|
console.log('API Base URL:', config.apiBaseUrl);
|
|
95
109
|
console.log('Device Type:', config.deviceType || 'auto-detected');
|
|
96
110
|
|
|
97
|
-
// Call status API
|
|
98
111
|
apiService.getSessionStatus()
|
|
99
112
|
.then((statusResponse) => {
|
|
100
113
|
console.log('=== Session Status API Response ===');
|
|
@@ -152,8 +165,7 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
152
165
|
/>
|
|
153
166
|
);
|
|
154
167
|
}
|
|
155
|
-
|
|
156
|
-
// Show session error if present
|
|
168
|
+
|
|
157
169
|
if (sessionError) {
|
|
158
170
|
return (
|
|
159
171
|
<div className="fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar">
|
|
@@ -19,6 +19,9 @@ export interface LivenessState {
|
|
|
19
19
|
lastResultsAt: number;
|
|
20
20
|
stage: LivenessStage;
|
|
21
21
|
livenessReady: boolean;
|
|
22
|
+
currentYaw: number | null;
|
|
23
|
+
currentAbsYaw: number | null;
|
|
24
|
+
livenessCompleted: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export class FaceMeshService {
|
|
@@ -111,6 +114,10 @@ export class FaceMeshService {
|
|
|
111
114
|
|
|
112
115
|
this.processLiveness(faceOnCanvas, w, h);
|
|
113
116
|
} else {
|
|
117
|
+
// Reset face orientation when no face is detected
|
|
118
|
+
this.livenessStateRef.current.currentYaw = null;
|
|
119
|
+
this.livenessStateRef.current.currentAbsYaw = null;
|
|
120
|
+
|
|
114
121
|
const vid = this.videoRef.current as HTMLVideoElement | null;
|
|
115
122
|
if (vid) {
|
|
116
123
|
const vidW = Math.max(1, vid?.videoWidth || displayW);
|
|
@@ -160,6 +167,10 @@ export class FaceMeshService {
|
|
|
160
167
|
const yaw = (nT.x - midX) / Math.max(1e-6, faceWidth);
|
|
161
168
|
const absYaw = Math.abs(yaw);
|
|
162
169
|
|
|
170
|
+
// Store current face orientation for capture validation
|
|
171
|
+
this.livenessStateRef.current.currentYaw = yaw;
|
|
172
|
+
this.livenessStateRef.current.currentAbsYaw = absYaw;
|
|
173
|
+
|
|
163
174
|
const xs = faceOnCanvas.map(p => p.x), ys = faceOnCanvas.map(p => p.y);
|
|
164
175
|
const minX = Math.min(...xs) * w, maxX = Math.max(...xs) * w;
|
|
165
176
|
const minY = Math.min(...ys) * h, maxY = Math.max(...ys) * h;
|
|
@@ -192,11 +203,14 @@ export class FaceMeshService {
|
|
|
192
203
|
} else if (absYaw < centerThreshold) {
|
|
193
204
|
state.centerHold += 1;
|
|
194
205
|
if (state.centerHold >= holdFramesCenter) {
|
|
195
|
-
|
|
196
|
-
state.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
// Only transition to LEFT if liveness check hasn't been completed yet
|
|
207
|
+
if (!state.livenessCompleted) {
|
|
208
|
+
const newStage: LivenessStage = "LEFT";
|
|
209
|
+
state.stage = newStage;
|
|
210
|
+
state.centerHold = 0;
|
|
211
|
+
if (this.callbacks.onLivenessUpdate) {
|
|
212
|
+
this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
|
|
213
|
+
}
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
} else {
|
|
@@ -235,16 +249,13 @@ export class FaceMeshService {
|
|
|
235
249
|
state.rightHold += 1;
|
|
236
250
|
if (state.rightHold >= holdFramesTurn) {
|
|
237
251
|
state.rightHold = 0;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (this.callbacks.onCaptureTrigger) {
|
|
246
|
-
this.callbacks.onCaptureTrigger();
|
|
247
|
-
}
|
|
252
|
+
// Mark liveness as completed and transition to DONE stage
|
|
253
|
+
state.livenessCompleted = true;
|
|
254
|
+
const newStage: LivenessStage = "DONE";
|
|
255
|
+
state.stage = newStage;
|
|
256
|
+
state.centerHold = 0;
|
|
257
|
+
if (this.callbacks.onLivenessUpdate) {
|
|
258
|
+
this.callbacks.onLivenessUpdate(newStage, "Great! Now look straight at the camera");
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
} else {
|
|
@@ -253,6 +264,27 @@ export class FaceMeshService {
|
|
|
253
264
|
this.callbacks.onLivenessUpdate(state.stage, yaw < -leftThreshold ? "You're facing left. Turn RIGHT" : "Turn a bit more RIGHT");
|
|
254
265
|
}
|
|
255
266
|
}
|
|
267
|
+
} else if (state.stage === "DONE") {
|
|
268
|
+
// In DONE stage, wait for face to be straight before capturing (no center check needed)
|
|
269
|
+
// Reduced threshold for easier capture when face is straight
|
|
270
|
+
const reducedThreshold = 0.08; // More lenient threshold
|
|
271
|
+
if (absYaw < reducedThreshold) {
|
|
272
|
+
state.centerHold += 1;
|
|
273
|
+
if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
|
|
274
|
+
state.snapTriggered = true;
|
|
275
|
+
if (this.callbacks.onLivenessUpdate) {
|
|
276
|
+
this.callbacks.onLivenessUpdate(state.stage, "Capturing...");
|
|
277
|
+
}
|
|
278
|
+
if (this.callbacks.onCaptureTrigger) {
|
|
279
|
+
this.callbacks.onCaptureTrigger();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
state.centerHold = 0;
|
|
284
|
+
if (this.callbacks.onLivenessUpdate) {
|
|
285
|
+
this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
290
|
}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* Handles all KYC-related API calls (face scan, document upload, status check)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export const COMPLETED_STEPS = {
|
|
7
|
+
INITIATED: "initiated",
|
|
8
|
+
FACE: "face_scan",
|
|
9
|
+
DOCS: "document_upload",
|
|
10
|
+
COMPLETED: "completed",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
6
13
|
export interface KycApiConfig {
|
|
7
14
|
apiBaseUrl: string;
|
|
8
15
|
sessionId: string;
|