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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astra-sdk-web",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "Official Astra SDK for JavaScript/TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/astra-sdk.cjs.js",
@@ -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.checkSessionActive();
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 has already been registered for this session. Proceeding to document upload.',
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.checkSessionActive();
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
- const newStage: LivenessStage = "LEFT";
196
- state.stage = newStage;
197
- state.centerHold = 0;
198
- if (this.callbacks.onLivenessUpdate) {
199
- this.callbacks.onLivenessUpdate(newStage, "Turn your face LEFT");
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
- if (!state.snapTriggered) {
239
- state.snapTriggered = true;
240
- const newStage: LivenessStage = "DONE";
241
- state.stage = newStage;
242
- if (this.callbacks.onLivenessUpdate) {
243
- this.callbacks.onLivenessUpdate(newStage, "Capturing...");
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;