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/astra-sdk.cjs.js +167 -18
- package/dist/astra-sdk.cjs.js.map +1 -1
- package/dist/astra-sdk.css +19 -0
- package/dist/astra-sdk.css.map +1 -1
- package/dist/astra-sdk.d.cts +4 -4
- package/dist/astra-sdk.es.js +167 -18
- package/dist/astra-sdk.es.js.map +1 -1
- package/dist/components.cjs.js +167 -18
- package/dist/components.cjs.js.map +1 -1
- package/dist/components.css +19 -0
- package/dist/components.css.map +1 -1
- package/dist/components.es.js +167 -18
- package/dist/components.es.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/package.json +1 -1
- package/src/features/faceScan/hooks/useFaceScan.ts +3 -3
- package/src/pages/DocumentUploadModal.tsx +63 -1
- package/src/pages/FaceScanModal.tsx +123 -9
- package/src/services/faceMeshService.ts +5 -7
- package/src/services/kycApiService.ts +41 -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;
|
|
@@ -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
|
@@ -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
|
|
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 >=
|
|
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.
|
|
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.
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|