astra-sdk-web 1.1.8 → 1.1.10

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;
@@ -125,6 +121,10 @@ declare class KycApiService {
125
121
  * Upload document scan image
126
122
  */
127
123
  uploadDocument(docBlob: Blob | File, docType: string): Promise<DocumentUploadResponse>;
124
+ /**
125
+ * Retry session - resets the session to allow face registration again
126
+ */
127
+ retrySession(): Promise<any>;
128
128
  /**
129
129
  * Check if session is active, throw error if not
130
130
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astra-sdk-web",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Official Astra SDK for JavaScript/TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/astra-sdk.cjs.js",
@@ -78,8 +78,8 @@ export function useFaceScan(
78
78
  const handleFaceCapture = useCallback(async () => {
79
79
  if (!videoRef.current) return;
80
80
 
81
- // Check if face is straight before capturing
82
- const centerThreshold = 0.05;
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
83
  const currentAbsYaw = livenessStateRef.current.currentAbsYaw;
84
84
 
85
85
  if (currentAbsYaw === null || currentAbsYaw === undefined) {
@@ -90,7 +90,7 @@ export function useFaceScan(
90
90
  return;
91
91
  }
92
92
 
93
- if (currentAbsYaw >= centerThreshold) {
93
+ if (currentAbsYaw >= reducedThreshold) {
94
94
  setState(prev => ({
95
95
  ...prev,
96
96
  livenessInstruction: "Please look straight at the camera before capturing",
@@ -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 {
@@ -12,6 +13,7 @@ function DocumentUploadModal({ onComplete }: DocumentUploadModalProps) {
12
13
  const navigate = useNavigate();
13
14
  const { apiService } = useKycContext();
14
15
  const [sessionError, setSessionError] = useState<string | null>(null);
16
+ const [kycCompleted, setKycCompleted] = useState(false);
15
17
 
16
18
  const {
17
19
  state,
@@ -28,6 +30,16 @@ function DocumentUploadModal({ onComplete }: DocumentUploadModalProps) {
28
30
  throw new Error('API service not initialized');
29
31
  }
30
32
  await apiService.uploadDocument(blob, docType);
33
+ // Check if KYC is completed after document upload
34
+ try {
35
+ const statusResponse = await apiService.getSessionStatus();
36
+ const { completed_steps, status } = statusResponse.data;
37
+ if (status === 'COMPLETED' || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) {
38
+ setKycCompleted(true);
39
+ }
40
+ } catch (error) {
41
+ console.error('Error checking completion status:', error);
42
+ }
31
43
  },
32
44
  onUpload: (file, docType) => {
33
45
  if (onComplete) {
@@ -47,7 +59,35 @@ function DocumentUploadModal({ onComplete }: DocumentUploadModalProps) {
47
59
  if (!apiService) return;
48
60
 
49
61
  try {
50
- await apiService.checkSessionActive();
62
+ const statusResponse = await apiService.getSessionStatus();
63
+ const { completed_steps, next_step, status } = statusResponse.data;
64
+
65
+ // Check if KYC is completed
66
+ if (status === 'COMPLETED' || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) {
67
+ setKycCompleted(true);
68
+ return;
69
+ }
70
+
71
+ // Check if session is active
72
+ if (status !== 'ACTIVE') {
73
+ throw new Error('Session expired or inactive');
74
+ }
75
+
76
+ // If document_upload is already completed, show completion message
77
+ if (completed_steps.includes(COMPLETED_STEPS.DOCS)) {
78
+ // Check if all steps are completed
79
+ if (completed_steps.includes(COMPLETED_STEPS.FACE) && completed_steps.includes(COMPLETED_STEPS.DOCS)) {
80
+ setKycCompleted(true);
81
+ return;
82
+ }
83
+ }
84
+
85
+ // If next_step is not document_upload and face_scan is not completed, redirect to face scan
86
+ if (next_step === COMPLETED_STEPS.FACE && !completed_steps.includes(COMPLETED_STEPS.FACE)) {
87
+ // Should not happen if we're in document upload modal, but handle it
88
+ console.warn('Face scan not completed, but in document upload modal');
89
+ }
90
+
51
91
  setSessionError(null);
52
92
  } catch (error: any) {
53
93
  const message = error.message || 'Session expired or inactive';
@@ -63,6 +103,28 @@ function DocumentUploadModal({ onComplete }: DocumentUploadModalProps) {
63
103
  }, [apiService, navigate]);
64
104
 
65
105
 
106
+ // Show KYC completion message
107
+ if (kycCompleted) {
108
+ return (
109
+ <div className="fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar">
110
+ <div className="max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl">
111
+ <div className="text-center">
112
+ <div className="mb-4 text-6xl">✅</div>
113
+ <h2 className="m-0 mb-4 text-[26px] font-bold text-green-500">
114
+ KYC Completed
115
+ </h2>
116
+ <p className="text-[#e5e7eb] mb-4 text-lg">
117
+ All steps have been completed successfully.
118
+ </p>
119
+ <p className="text-[#9ca3af] text-sm mb-6">
120
+ Please return to your desktop to continue.
121
+ </p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
66
128
  // Show session error if present
67
129
  if (sessionError) {
68
130
  return (
@@ -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 {
@@ -18,6 +19,9 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
18
19
  const { apiService } = useKycContext();
19
20
  const [sessionError, setSessionError] = useState<string | null>(null);
20
21
  const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'info' | 'warning' } | null>(null);
22
+ const [showRetryButton, setShowRetryButton] = useState(false);
23
+ const [isRetrying, setIsRetrying] = useState(false);
24
+ const [kycCompleted, setKycCompleted] = useState(false);
21
25
 
22
26
  const { videoRef, cameraReady, stopCamera } = useCamera();
23
27
  const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, {
@@ -37,10 +41,12 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
37
41
  errorData?.message?.includes('Face already registered') ||
38
42
  (error as any)?.statusCode === 500 && errorMessage.includes('Face')
39
43
  ) {
44
+ setShowRetryButton(true);
40
45
  setToast({
41
- message: 'Face already registered',
46
+ message: 'Face already registered. Click Retry to register again.',
42
47
  type: 'warning',
43
48
  });
49
+ setState(prev => ({ ...prev, loading: false }));
44
50
  return;
45
51
  }
46
52
  throw error;
@@ -58,7 +64,34 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
58
64
  if (!apiService) return;
59
65
 
60
66
  try {
61
- await apiService.checkSessionActive();
67
+ const statusResponse = await apiService.getSessionStatus();
68
+ const { completed_steps, next_step, status } = statusResponse.data;
69
+
70
+ // Check if session is active
71
+ if (status !== 'ACTIVE') {
72
+ throw new Error('Session expired or inactive');
73
+ }
74
+
75
+ // Check if KYC is completed
76
+ if (status === 'COMPLETED' || completed_steps.includes(COMPLETED_STEPS.COMPLETED)) {
77
+ setKycCompleted(true);
78
+ return;
79
+ }
80
+
81
+ // If face_scan is already completed, skip to document upload
82
+ if (completed_steps.includes(COMPLETED_STEPS.FACE)) {
83
+ setState(prev => ({ ...prev, showDocumentUpload: true }));
84
+ return;
85
+ }
86
+
87
+ // If next_step is not face_scan, redirect accordingly
88
+ if (next_step !== COMPLETED_STEPS.FACE && next_step !== COMPLETED_STEPS.INITIATED) {
89
+ if (next_step === COMPLETED_STEPS.DOCS) {
90
+ setState(prev => ({ ...prev, showDocumentUpload: true }));
91
+ return;
92
+ }
93
+ }
94
+
62
95
  setSessionError(null);
63
96
  } catch (error: any) {
64
97
  const message = error.message || 'Session expired or inactive';
@@ -70,7 +103,7 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
70
103
  };
71
104
 
72
105
  checkSession();
73
- }, [apiService, navigate]);
106
+ }, [apiService, navigate, setState]);
74
107
 
75
108
  useEffect(() => {
76
109
  setState(prev => ({ ...prev, cameraReady }));
@@ -103,7 +136,55 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
103
136
  }
104
137
  }, [cameraReady, apiService]);
105
138
 
106
- const handleRetry = () => {
139
+ const handleRetry = async () => {
140
+ if (!apiService || isRetrying) return;
141
+
142
+ setIsRetrying(true);
143
+ setShowRetryButton(false);
144
+ setToast(null);
145
+
146
+ try {
147
+ await apiService.retrySession();
148
+
149
+ // Reset face scan state
150
+ stopCamera();
151
+ setState({
152
+ cameraReady: false,
153
+ livenessStage: 'CENTER',
154
+ livenessReady: false,
155
+ livenessFailed: false,
156
+ modelLoading: true,
157
+ modelLoaded: false,
158
+ livenessInstruction: 'Look straight at the camera',
159
+ loading: false,
160
+ allStepsCompleted: false,
161
+ capturedImage: null,
162
+ showDocumentUpload: false,
163
+ });
164
+
165
+ refs.centerHold.current = 0;
166
+ refs.leftHold.current = 0;
167
+ refs.rightHold.current = 0;
168
+ refs.snapTriggered.current = false;
169
+ refs.lastResultsAt.current = 0;
170
+ refs.modelLoaded.current = false;
171
+ refs.livenessFailed.current = false;
172
+
173
+ // Reload to restart face scan
174
+ setTimeout(() => {
175
+ window.location.reload();
176
+ }, 500);
177
+ } catch (error: any) {
178
+ setIsRetrying(false);
179
+ setShowRetryButton(true);
180
+ setToast({
181
+ message: error?.message || 'Retry failed. Please try again.',
182
+ type: 'error',
183
+ });
184
+ }
185
+ };
186
+
187
+ const handleRestart = () => {
107
188
  stopCamera();
108
189
  setState({
109
190
  cameraReady: false,
@@ -132,6 +213,28 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
132
213
  }, 100);
133
214
  };
134
215
 
216
+ // Show KYC completion message
217
+ if (kycCompleted) {
218
+ return (
219
+ <div className="fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar">
220
+ <div className="max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl">
221
+ <div className="text-center">
222
+ <div className="mb-4 text-6xl">✅</div>
223
+ <h2 className="m-0 mb-4 text-[26px] font-bold text-green-500">
224
+ KYC Completed
225
+ </h2>
226
+ <p className="text-[#e5e7eb] mb-4 text-lg">
227
+ All steps have been completed successfully.
228
+ </p>
229
+ <p className="text-[#9ca3af] text-sm mb-6">
230
+ Please return to your desktop to continue.
231
+ </p>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ );
236
+ }
237
+
135
238
  if (state.showDocumentUpload) {
136
239
  return (
137
240
  <DocumentUploadModal
@@ -224,12 +327,23 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
224
327
  </div>
225
328
  )}
226
329
 
330
+ {showRetryButton && (
331
+ <button
332
+ type="button"
333
+ onClick={handleRetry}
334
+ disabled={isRetrying || state.loading}
335
+ className="py-3.5 px-4 rounded-xl text-base font-bold border-none transition-colors bg-[#f59e0b] text-[#0b0f17] cursor-pointer hover:bg-[#d97706] disabled:opacity-50 disabled:cursor-not-allowed"
336
+ >
337
+ {isRetrying ? "Retrying..." : "Retry Face Registration"}
338
+ </button>
339
+ )}
340
+
227
341
  <button
228
342
  type="button"
229
- disabled={!state.cameraReady || state.loading || (!state.livenessFailed && state.livenessStage !== "DONE")}
343
+ disabled={!state.cameraReady || state.loading || (!state.livenessFailed && state.livenessStage !== "DONE") || showRetryButton}
230
344
  onClick={handleFaceCapture}
231
345
  className={`py-3.5 px-4 rounded-xl text-base font-bold border-none transition-colors ${
232
- state.cameraReady && !state.loading && (state.livenessFailed || state.livenessStage === "DONE")
346
+ state.cameraReady && !state.loading && (state.livenessFailed || state.livenessStage === "DONE") && !showRetryButton
233
347
  ? "bg-[#22c55e] text-[#0b0f17] cursor-pointer hover:bg-[#16a34a]"
234
348
  : "bg-[#374151] text-[#e5e7eb] cursor-not-allowed"
235
349
  }`}
@@ -243,10 +357,10 @@ function FaceScanModal({ onComplete }: FaceScanModalProps) {
243
357
 
244
358
  <button
245
359
  type="button"
246
- onClick={handleRetry}
247
- disabled={state.loading}
360
+ onClick={handleRestart}
361
+ disabled={state.loading || isRetrying}
248
362
  className={`py-3 px-4 rounded-[10px] text-[15px] font-semibold border-none w-full transition-colors ${
249
- state.loading ? "bg-[#374151] text-[#e5e7eb] cursor-not-allowed opacity-50" : "bg-[#374151] text-[#e5e7eb] cursor-pointer hover:bg-[#4b5563]"
363
+ (state.loading || isRetrying) ? "bg-[#374151] text-[#e5e7eb] cursor-not-allowed opacity-50" : "bg-[#374151] text-[#e5e7eb] cursor-pointer hover:bg-[#4b5563]"
250
364
  }`}
251
365
  >
252
366
  Restart
@@ -265,8 +265,10 @@ export class FaceMeshService {
265
265
  }
266
266
  }
267
267
  } else if (state.stage === "DONE") {
268
- // In DONE stage, wait for face to be straight before capturing
269
- if (absYaw < centerThreshold && insideGuide) {
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) {
270
272
  state.centerHold += 1;
271
273
  if (state.centerHold >= holdFramesCenter && !state.snapTriggered) {
272
274
  state.snapTriggered = true;
@@ -280,11 +282,7 @@ export class FaceMeshService {
280
282
  } else {
281
283
  state.centerHold = 0;
282
284
  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
- }
285
+ this.callbacks.onLivenessUpdate(state.stage, "Please look straight at the camera");
288
286
  }
289
287
  }
290
288
  }
@@ -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;
@@ -170,6 +177,40 @@ export class KycApiService {
170
177
  }
171
178
  }
172
179
 
180
+ /**
181
+ * Retry session - resets the session to allow face registration again
182
+ */
183
+ async retrySession(): Promise<any> {
184
+ const deviceType = this.config.deviceType || this.detectDeviceType();
185
+
186
+ try {
187
+ const response = await fetch(
188
+ `${this.config.apiBaseUrl}/api/v2/dashboard/merchant/onsite/session/${this.config.sessionId}/retry`,
189
+ {
190
+ method: 'POST',
191
+ headers: {
192
+ 'x-server-key': this.config.serverKey,
193
+ 'device-type': deviceType,
194
+ 'Content-Type': 'application/json',
195
+ },
196
+ credentials: 'include',
197
+ }
198
+ );
199
+
200
+ if (!response.ok) {
201
+ const errorData = await response.json().catch(() => ({}));
202
+ const message = errorData?.message || `Retry failed with status ${response.status}`;
203
+ throw new Error(message);
204
+ }
205
+
206
+ const data = await response.json();
207
+ return data;
208
+ } catch (error: any) {
209
+ const message = error?.message || 'Retry failed';
210
+ throw new Error(`Retry failed: ${message}`);
211
+ }
212
+ }
213
+
173
214
  /**
174
215
  * Check if session is active, throw error if not
175
216
  */