astra-sdk-web 1.1.7 → 1.1.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/astra-sdk.cjs.js +60 -17
- 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.es.js +60 -17
- package/dist/astra-sdk.es.js.map +1 -1
- package/dist/components.cjs.js +60 -17
- 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 +60 -17
- package/dist/components.es.js.map +1 -1
- package/package.json +1 -1
- package/src/features/faceScan/hooks/useFaceScan.ts +24 -0
- package/src/pages/FaceScanModal.tsx +2 -12
- package/src/services/faceMeshService.ts +49 -15
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
|
|
82
|
+
const centerThreshold = 0.05;
|
|
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 >= centerThreshold) {
|
|
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;
|
|
@@ -28,7 +28,6 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
28
28
|
try {
|
|
29
29
|
await apiService.uploadFaceScan(blob);
|
|
30
30
|
} catch (error: any) {
|
|
31
|
-
// Check if it's a "Face already registered" error
|
|
32
31
|
const errorMessage = error?.message || '';
|
|
33
32
|
const errorData = (error as any)?.errorData || {};
|
|
34
33
|
|
|
@@ -39,13 +38,11 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
39
38
|
(error as any)?.statusCode === 500 && errorMessage.includes('Face')
|
|
40
39
|
) {
|
|
41
40
|
setToast({
|
|
42
|
-
message: 'Face
|
|
41
|
+
message: 'Face already registered',
|
|
43
42
|
type: 'warning',
|
|
44
43
|
});
|
|
45
|
-
// Don't throw error - allow flow to continue to document upload
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
|
-
// Re-throw other errors
|
|
49
46
|
throw error;
|
|
50
47
|
}
|
|
51
48
|
},
|
|
@@ -56,7 +53,6 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
56
53
|
},
|
|
57
54
|
});
|
|
58
55
|
|
|
59
|
-
// Check session status on mount
|
|
60
56
|
useEffect(() => {
|
|
61
57
|
const checkSession = async () => {
|
|
62
58
|
if (!apiService) return;
|
|
@@ -67,7 +63,6 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
67
63
|
} catch (error: any) {
|
|
68
64
|
const message = error.message || 'Session expired or inactive';
|
|
69
65
|
setSessionError(message);
|
|
70
|
-
// Redirect to QR page after showing error
|
|
71
66
|
setTimeout(() => {
|
|
72
67
|
navigate('/qr', { replace: true });
|
|
73
68
|
}, 2000);
|
|
@@ -77,15 +72,12 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
77
72
|
checkSession();
|
|
78
73
|
}, [apiService, navigate]);
|
|
79
74
|
|
|
80
|
-
// Sync camera ready state
|
|
81
75
|
useEffect(() => {
|
|
82
76
|
setState(prev => ({ ...prev, cameraReady }));
|
|
83
77
|
}, [cameraReady, setState]);
|
|
84
78
|
|
|
85
|
-
// Log session info and call status API when camera opens
|
|
86
79
|
useEffect(() => {
|
|
87
80
|
if (cameraReady && apiService) {
|
|
88
|
-
// Get config from apiService to log session info
|
|
89
81
|
const config = apiService.getConfig();
|
|
90
82
|
if (config) {
|
|
91
83
|
console.log('=== Camera Opened ===');
|
|
@@ -94,7 +86,6 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
94
86
|
console.log('API Base URL:', config.apiBaseUrl);
|
|
95
87
|
console.log('Device Type:', config.deviceType || 'auto-detected');
|
|
96
88
|
|
|
97
|
-
// Call status API
|
|
98
89
|
apiService.getSessionStatus()
|
|
99
90
|
.then((statusResponse) => {
|
|
100
91
|
console.log('=== Session Status API Response ===');
|
|
@@ -152,8 +143,7 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
|
|
|
152
143
|
/>
|
|
153
144
|
);
|
|
154
145
|
}
|
|
155
|
-
|
|
156
|
-
// Show session error if present
|
|
146
|
+
|
|
157
147
|
if (sessionError) {
|
|
158
148
|
return (
|
|
159
149
|
<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,29 @@ 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
|
|
269
|
+
if (absYaw < centerThreshold && insideGuide) {
|
|
270
|
+
state.centerHold += 1;
|
|
271
|
+
if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
|
|
272
|
+
state.snapTriggered = true;
|
|
273
|
+
if (this.callbacks.onLivenessUpdate) {
|
|
274
|
+
this.callbacks.onLivenessUpdate(state.stage, "Capturing...");
|
|
275
|
+
}
|
|
276
|
+
if (this.callbacks.onCaptureTrigger) {
|
|
277
|
+
this.callbacks.onCaptureTrigger();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
state.centerHold = 0;
|
|
282
|
+
if (this.callbacks.onLivenessUpdate) {
|
|
283
|
+
if (!insideGuide) {
|
|
284
|
+
this.callbacks.onLivenessUpdate(state.stage, "Center your face inside the circle");
|
|
285
|
+
} else {
|
|
286
|
+
this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
256
290
|
}
|
|
257
291
|
}
|
|
258
292
|
}
|