astra-sdk-web 1.0.0 → 1.1.1

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.
Files changed (43) hide show
  1. package/dist/.htaccess +9 -0
  2. package/dist/astra-sdk.cjs.js +1719 -1
  3. package/dist/astra-sdk.cjs.js.map +1 -1
  4. package/dist/astra-sdk.css +934 -0
  5. package/dist/astra-sdk.css.map +1 -0
  6. package/dist/astra-sdk.d.cts +155 -0
  7. package/dist/astra-sdk.es.js +1706 -1
  8. package/dist/astra-sdk.es.js.map +1 -1
  9. package/dist/components.cjs.js +1473 -0
  10. package/dist/components.cjs.js.map +1 -0
  11. package/dist/components.css +934 -0
  12. package/dist/components.css.map +1 -0
  13. package/dist/components.d.cts +13 -0
  14. package/dist/components.d.ts +13 -0
  15. package/dist/components.es.js +1467 -0
  16. package/dist/components.es.js.map +1 -0
  17. package/dist/index.d.ts +155 -115
  18. package/package.json +24 -2
  19. package/src/App.tsx +12 -8
  20. package/src/components/KycFlow.tsx +41 -0
  21. package/src/components/index.ts +3 -0
  22. package/src/contexts/KycContext.tsx +66 -0
  23. package/src/features/documentUpload/hooks/useDocumentUpload.ts +226 -0
  24. package/src/features/documentUpload/index.ts +3 -0
  25. package/src/features/documentUpload/types.ts +16 -0
  26. package/src/features/faceScan/hooks/useCamera.ts +62 -0
  27. package/src/features/faceScan/hooks/useFaceScan.ts +249 -0
  28. package/src/features/faceScan/index.ts +4 -0
  29. package/src/features/faceScan/types.ts +29 -0
  30. package/src/index.css +13 -62
  31. package/src/pages/DocumentUploadModal.tsx +262 -0
  32. package/src/pages/FaceScanModal.tsx +207 -0
  33. package/src/pages/MobileRoute.tsx +42 -0
  34. package/src/pages/QRCodePage.tsx +125 -0
  35. package/src/sdk/index.ts +18 -30
  36. package/src/services/faceMeshService.ts +382 -0
  37. package/src/services/index.ts +5 -0
  38. package/src/services/kycApiService.ts +194 -0
  39. package/src/utils/deviceDetection.ts +28 -0
  40. package/dist/astra-sdk.umd.js +0 -2
  41. package/dist/astra-sdk.umd.js.map +0 -1
  42. package/dist/vite.svg +0 -1
  43. package/src/App.css +0 -42
@@ -0,0 +1,207 @@
1
+ import { useRef, useEffect, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import DocumentUploadModal from './DocumentUploadModal';
4
+ import { useCamera } from '../features/faceScan/hooks/useCamera';
5
+ import { useFaceScan } from '../features/faceScan/hooks/useFaceScan';
6
+ import { useKycContext } from '../contexts/KycContext';
7
+ import '../index.css';
8
+
9
+ interface FaceScanModalProps {
10
+ onClose: () => void;
11
+ onComplete?: (capturedImage: string) => void;
12
+ }
13
+
14
+ function FaceScanModal({ onComplete }: FaceScanModalProps) {
15
+ const faceCanvasRef = useRef<HTMLCanvasElement>(null);
16
+ const navigate = useNavigate();
17
+ const { apiService } = useKycContext();
18
+ const [sessionError, setSessionError] = useState<string | null>(null);
19
+
20
+ const { videoRef, cameraReady, stopCamera } = useCamera();
21
+ const { state, setState, refs, handleFaceCapture } = useFaceScan(videoRef, faceCanvasRef, {
22
+ onFaceUpload: async (blob: Blob) => {
23
+ if (!apiService) {
24
+ throw new Error('API service not initialized');
25
+ }
26
+ await apiService.uploadFaceScan(blob);
27
+ },
28
+ onFaceCaptureComplete: (imageData: string) => {
29
+ if (onComplete) {
30
+ onComplete(imageData);
31
+ }
32
+ },
33
+ });
34
+
35
+ // Check session status on mount
36
+ useEffect(() => {
37
+ const checkSession = async () => {
38
+ if (!apiService) return;
39
+
40
+ try {
41
+ await apiService.checkSessionActive();
42
+ setSessionError(null);
43
+ } catch (error: any) {
44
+ const message = error.message || 'Session expired or inactive';
45
+ setSessionError(message);
46
+ // Redirect to QR page after showing error
47
+ setTimeout(() => {
48
+ navigate('/qr', { replace: true });
49
+ }, 2000);
50
+ }
51
+ };
52
+
53
+ checkSession();
54
+ }, [apiService, navigate]);
55
+
56
+ // Sync camera ready state
57
+ useEffect(() => {
58
+ setState(prev => ({ ...prev, cameraReady }));
59
+ }, [cameraReady, setState]);
60
+
61
+ const handleRetry = () => {
62
+ stopCamera();
63
+ setState({
64
+ cameraReady: false,
65
+ livenessStage: 'CENTER',
66
+ livenessReady: false,
67
+ livenessFailed: false,
68
+ modelLoading: true,
69
+ modelLoaded: false,
70
+ livenessInstruction: 'Look straight at the camera',
71
+ loading: false,
72
+ allStepsCompleted: false,
73
+ capturedImage: null,
74
+ showDocumentUpload: false,
75
+ });
76
+
77
+ refs.centerHold.current = 0;
78
+ refs.leftHold.current = 0;
79
+ refs.rightHold.current = 0;
80
+ refs.snapTriggered.current = false;
81
+ refs.lastResultsAt.current = 0;
82
+ refs.modelLoaded.current = false;
83
+ refs.livenessFailed.current = false;
84
+
85
+ setTimeout(() => {
86
+ window.location.reload();
87
+ }, 100);
88
+ };
89
+
90
+ if (state.showDocumentUpload) {
91
+ return (
92
+ <DocumentUploadModal
93
+ onComplete={(_file, _docType) => {
94
+ if (onComplete && state.capturedImage) {
95
+ onComplete(state.capturedImage);
96
+ }
97
+ }}
98
+ />
99
+ );
100
+ }
101
+
102
+ // Show session error if present
103
+ if (sessionError) {
104
+ return (
105
+ <div className="fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar">
106
+ <div className="max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl">
107
+ <div className="text-center">
108
+ <h2 className="m-0 mb-4 text-[26px] font-bold text-red-500">
109
+ Session Expired
110
+ </h2>
111
+ <p className="text-[#e5e7eb] mb-4">{sessionError}</p>
112
+ <p className="text-[#9ca3af] text-sm">Redirecting to QR code page...</p>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ return (
120
+ <div className="fixed inset-0 bg-black p-5 z-[1000] flex items-center justify-center font-sans overflow-y-auto custom__scrollbar">
121
+ <div className="max-w-[400px] w-full mx-auto bg-[#0b0f17] rounded-2xl p-6 shadow-xl mt-48">
122
+ <div className="relative mb-4">
123
+ <h2 className="m-0 mb-4 text-[26px] font-bold text-white text-center">
124
+ Capture Face
125
+ </h2>
126
+ </div>
127
+
128
+ <div className="grid gap-4">
129
+ {!state.modelLoading && state.modelLoaded && !state.livenessFailed && (
130
+ <div className="bg-[#0a2315] text-[#34d399] py-3.5 px-4 rounded-xl text-sm border border-[#155e3b] text-left">
131
+ Face detection model loaded.
132
+ </div>
133
+ )}
134
+ {state.modelLoading && !state.livenessFailed && (
135
+ <div className="bg-[#1f2937] text-[#e5e7eb] py-3.5 px-4 rounded-xl text-sm border border-[#374151] text-left">
136
+ Loading face detection model...
137
+ </div>
138
+ )}
139
+
140
+ <div className="relative w-full aspect-square rounded-full overflow-hidden bg-black">
141
+ <video
142
+ ref={videoRef}
143
+ playsInline
144
+ muted
145
+ className="w-full h-full block bg-black object-cover -scale-x-100 origin-center"
146
+ />
147
+ <canvas
148
+ ref={faceCanvasRef}
149
+ className="absolute top-0 left-0 w-full h-full pointer-events-none z-[2]"
150
+ />
151
+ <svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 pointer-events-none z-[3]">
152
+ <circle cx="50" cy="50" r="44" fill="none" stroke="#22c55e" strokeWidth="2" strokeDasharray="1 3" />
153
+ </svg>
154
+ </div>
155
+
156
+ {!state.livenessFailed && (
157
+ <div className="bg-gradient-to-b from-[rgba(17,24,39,0.9)] to-[rgba(17,24,39,0.6)] text-[#e5e7eb] p-4 rounded-2xl text-base border border-[#30363d]">
158
+ <div className="font-bold mb-2.5 text-[22px] text-white">Liveness Check</div>
159
+ <div className="mb-2.5 text-base">{state.livenessInstruction}</div>
160
+ <div className="grid gap-2.5 text-lg">
161
+ <div className={state.livenessStage === "CENTER" || state.livenessStage === "LEFT" || state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40"}>
162
+ 1. Look Straight
163
+ </div>
164
+ <div className={state.livenessStage === "RIGHT" || state.livenessStage === "DONE" ? "opacity-100" : "opacity-40"}>
165
+ 2. Turn your face right
166
+ </div>
167
+ <div className={state.livenessStage === "DONE" ? "opacity-100" : "opacity-30"}>
168
+ 3. Turn your face left
169
+ </div>
170
+ </div>
171
+ </div>
172
+ )}
173
+
174
+ <button
175
+ type="button"
176
+ disabled={!state.cameraReady || state.loading || (!state.livenessFailed && state.livenessStage !== "DONE")}
177
+ onClick={handleFaceCapture}
178
+ className={`py-3.5 px-4 rounded-xl text-base font-bold border-none transition-colors ${
179
+ state.cameraReady && !state.loading && (state.livenessFailed || state.livenessStage === "DONE")
180
+ ? "bg-[#22c55e] text-[#0b0f17] cursor-pointer hover:bg-[#16a34a]"
181
+ : "bg-[#374151] text-[#e5e7eb] cursor-not-allowed"
182
+ }`}
183
+ >
184
+ {state.loading
185
+ ? "Capturing..."
186
+ : (state.livenessFailed || state.livenessStage === "DONE")
187
+ ? "Capture & Continue"
188
+ : "Complete steps to continue"}
189
+ </button>
190
+
191
+ <button
192
+ type="button"
193
+ onClick={handleRetry}
194
+ disabled={state.loading}
195
+ className={`py-3 px-4 rounded-[10px] text-[15px] font-semibold border-none w-full transition-colors ${
196
+ state.loading ? "bg-[#374151] text-[#e5e7eb] cursor-not-allowed opacity-50" : "bg-[#374151] text-[#e5e7eb] cursor-pointer hover:bg-[#4b5563]"
197
+ }`}
198
+ >
199
+ Restart
200
+ </button>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ );
205
+ }
206
+
207
+ export default FaceScanModal;
@@ -0,0 +1,42 @@
1
+ import { useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { isMobileDevice } from '../utils/deviceDetection';
4
+ import FaceScanModal from './FaceScanModal';
5
+ import '../index.css';
6
+
7
+ interface MobileRouteProps {
8
+ onClose?: () => void;
9
+ }
10
+
11
+ function MobileRoute({ onClose }: MobileRouteProps = {}) {
12
+ const navigate = useNavigate();
13
+
14
+ useEffect(() => {
15
+ if (!isMobileDevice()) {
16
+ navigate('/qr', { replace: true });
17
+ return;
18
+ }
19
+ }, [navigate]);
20
+
21
+ const handleClose = () => {
22
+ if (onClose) {
23
+ onClose();
24
+ } else {
25
+ navigate(-1);
26
+ }
27
+ };
28
+
29
+ const handleComplete = (capturedImage: string) => {
30
+ // Handle completion - send captured image to backend or parent
31
+ console.log('Face capture completed with image:', capturedImage.substring(0, 50) + '...');
32
+ // You can emit an event, call a callback, or navigate here
33
+ };
34
+
35
+ if (!isMobileDevice()) {
36
+ return null;
37
+ }
38
+
39
+ return <FaceScanModal onClose={handleClose} onComplete={handleComplete} />;
40
+ }
41
+
42
+ export default MobileRoute;
@@ -0,0 +1,125 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { QRCodeSVG } from 'qrcode.react';
3
+ import { useSearchParams, useNavigate } from 'react-router-dom';
4
+ import '../index.css';
5
+
6
+ interface QRCodePageProps {
7
+ onClose?: () => void;
8
+ }
9
+
10
+ function QRCodePage({ onClose }: QRCodePageProps = {}) {
11
+ const [searchParams] = useSearchParams();
12
+ const navigate = useNavigate();
13
+ const [qrUrl, setQrUrl] = useState<string>('');
14
+ const [copied, setCopied] = useState<boolean>(false);
15
+
16
+ useEffect(() => {
17
+ const currentUrl = window.location.origin;
18
+
19
+ const params: Record<string, string> = {};
20
+ searchParams.forEach((value, key) => {
21
+ params[key] = value;
22
+ });
23
+
24
+ const mobileRoute = '/mobileroute';
25
+ const queryString = new URLSearchParams(params).toString();
26
+ const fullUrl = `${currentUrl}${mobileRoute}${queryString ? `?${queryString}` : ''}`;
27
+
28
+ setQrUrl(fullUrl);
29
+ }, [searchParams]);
30
+
31
+ const handleCopyUrl = async () => {
32
+ if (qrUrl) {
33
+ try {
34
+ await navigator.clipboard.writeText(qrUrl);
35
+ setCopied(true);
36
+ setTimeout(() => setCopied(false), 2000);
37
+ } catch (err) {
38
+ console.error('Failed to copy:', err);
39
+ }
40
+ }
41
+ };
42
+
43
+ const handleClose = () => {
44
+ if (onClose) {
45
+ onClose();
46
+ } else {
47
+ navigate(-1);
48
+ }
49
+ };
50
+
51
+ const handleRefresh = () => {
52
+ window.location.reload();
53
+ };
54
+
55
+ return (
56
+ <div className="fixed inset-0 flex items-center justify-center bg-black p-4 sm:p-6 md:p-8 z-[1000]">
57
+ <div className="relative bg-[rgba(20,20,20,0.95)] rounded-2xl p-5 sm:p-6 md:p-7 max-w-[450px] w-full h-[80vh] flex flex-col text-center shadow-[0_8px_32px_rgba(0,0,0,0.5)]">
58
+ <button
59
+ className="absolute top-5 right-5 bg-transparent border-none text-white cursor-pointer p-2 flex items-center justify-center rounded transition-colors hover:bg-white/10 active:bg-white/20"
60
+ onClick={handleClose}
61
+ aria-label="Close"
62
+ >
63
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
64
+ <line x1="18" y1="6" x2="6" y2="18"></line>
65
+ <line x1="6" y1="6" x2="18" y2="18"></line>
66
+ </svg>
67
+ </button>
68
+
69
+ <div className="flex flex-col items-center gap-3 sm:gap-4 flex-1 overflow-y-auto custom__scrollbar">
70
+ <h1 className="m-0 text-white text-xl sm:text-2xl font-semibold leading-tight">Continue on Mobile</h1>
71
+ <p className="m-0 text-white text-sm sm:text-base opacity-90 leading-relaxed">
72
+ Scan this QR on your phone to capture your face and document
73
+ </p>
74
+
75
+ {qrUrl && (
76
+ <div className="flex justify-center items-center p-3 sm:p-4 bg-white rounded-xl border-2 border-white">
77
+ <QRCodeSVG
78
+ value={qrUrl}
79
+ size={180}
80
+ level="H"
81
+ includeMargin={true}
82
+ bgColor="transparent"
83
+ fgColor="#000000"
84
+ />
85
+ </div>
86
+ )}
87
+
88
+ <div className="w-full text-left mt-auto">
89
+ <p className="m-0 mb-2 text-white text-xs sm:text-sm opacity-80">Or open:</p>
90
+ <div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 p-2 sm:p-3 bg-white/10 rounded-lg border border-white/20">
91
+ <code className="flex-1 text-white text-xs sm:text-sm break-all text-left m-0 font-mono">{qrUrl}</code>
92
+ <button
93
+ className="bg-transparent border-none text-white cursor-pointer p-1.5 flex items-center justify-center rounded transition-colors flex-shrink-0 hover:bg-white/10 active:bg-white/20 self-end sm:self-auto"
94
+ onClick={handleCopyUrl}
95
+ aria-label="Copy URL"
96
+ title={copied ? 'Copied!' : 'Copy URL'}
97
+ >
98
+ {copied ? (
99
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
100
+ <polyline points="20 6 9 17 4 12"></polyline>
101
+ </svg>
102
+ ) : (
103
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
104
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
105
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
106
+ </svg>
107
+ )}
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ <button
113
+ className="w-full py-3 sm:py-4 px-6 bg-gradient-to-r from-[#FF842D] to-[#FF2D55] border-none rounded-lg text-white text-sm sm:text-base font-semibold cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-[0_4px_12px_rgba(255,107,53,0.4)] active:translate-y-0"
114
+ onClick={handleRefresh}
115
+ >
116
+ I've completed on mobile - Refresh
117
+ </button>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ export default QRCodePage;
125
+
package/src/sdk/index.ts CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * Astra SDK - Main entry point
3
- */
4
1
 
5
2
  import { ApiClient } from './client';
6
3
  import { mergeConfig } from './config';
@@ -19,23 +16,17 @@ export class AstraSDK {
19
16
  this.client = new ApiClient(mergedConfig);
20
17
  }
21
18
 
22
- /**
23
- * Get the underlying API client
24
- */
19
+
25
20
  getClient(): ApiClient {
26
21
  return this.client;
27
22
  }
28
23
 
29
- /**
30
- * Update SDK configuration
31
- */
24
+
32
25
  updateConfig(config: Partial<AstraSDKConfig>): void {
33
26
  this.client.updateConfig(config);
34
27
  }
35
28
 
36
- /**
37
- * Make a GET request
38
- */
29
+
39
30
  async get<T = unknown>(
40
31
  endpoint: string,
41
32
  options?: Omit<RequestOptions, 'method' | 'body'>
@@ -43,9 +34,7 @@ export class AstraSDK {
43
34
  return this.client.get<T>(endpoint, options);
44
35
  }
45
36
 
46
- /**
47
- * Make a POST request
48
- */
37
+
49
38
  async post<T = unknown>(
50
39
  endpoint: string,
51
40
  body?: unknown,
@@ -54,9 +43,6 @@ export class AstraSDK {
54
43
  return this.client.post<T>(endpoint, body, options);
55
44
  }
56
45
 
57
- /**
58
- * Make a PUT request
59
- */
60
46
  async put<T = unknown>(
61
47
  endpoint: string,
62
48
  body?: unknown,
@@ -65,9 +51,6 @@ export class AstraSDK {
65
51
  return this.client.put<T>(endpoint, body, options);
66
52
  }
67
53
 
68
- /**
69
- * Make a PATCH request
70
- */
71
54
  async patch<T = unknown>(
72
55
  endpoint: string,
73
56
  body?: unknown,
@@ -76,9 +59,6 @@ export class AstraSDK {
76
59
  return this.client.patch<T>(endpoint, body, options);
77
60
  }
78
61
 
79
- /**
80
- * Make a DELETE request
81
- */
82
62
  async delete<T = unknown>(
83
63
  endpoint: string,
84
64
  options?: Omit<RequestOptions, 'method' | 'body'>
@@ -86,9 +66,7 @@ export class AstraSDK {
86
66
  return this.client.delete<T>(endpoint, options);
87
67
  }
88
68
 
89
- /**
90
- * Make a generic request
91
- */
69
+
92
70
  async request<T = unknown>(
93
71
  endpoint: string,
94
72
  options?: RequestOptions
@@ -97,13 +75,23 @@ export class AstraSDK {
97
75
  }
98
76
  }
99
77
 
100
- // Export types
101
78
  export type { AstraSDKConfig, ApiResponse, RequestOptions, ApiError } from './types';
102
79
  export { AstraSDKError } from './types';
103
80
 
104
- // Export client for advanced usage
105
81
  export { ApiClient } from './client';
106
82
 
107
- // Default export
83
+ // Export KYC components
84
+ export { KycFlow } from '../components/KycFlow';
85
+ export type { KycFlowProps } from '../components/KycFlow';
86
+
87
+ // Export KYC API service
88
+ export { KycApiService } from '../services/kycApiService';
89
+ export type {
90
+ KycApiConfig,
91
+ SessionStatusResponse,
92
+ FaceScanResponse,
93
+ DocumentUploadResponse
94
+ } from '../services/kycApiService';
95
+
108
96
  export default AstraSDK;
109
97