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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astra-sdk-web",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Official Astra SDK for JavaScript/TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/astra-sdk.cjs.js",
@@ -11,6 +11,11 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/astra-sdk.es.js",
13
13
  "require": "./dist/astra-sdk.cjs.js"
14
+ },
15
+ "./components": {
16
+ "types": "./dist/components.d.ts",
17
+ "import": "./dist/components.es.js",
18
+ "require": "./dist/components.cjs.js"
14
19
  }
15
20
  },
16
21
  "files": [
@@ -28,7 +33,7 @@
28
33
  "scripts": {
29
34
  "dev": "vite",
30
35
  "build": "tsc && vite build",
31
- "build:lib": "tsc && vite build",
36
+ "build:lib": "tsup && node scripts/copy-assets.mjs",
32
37
  "lint": "eslint .",
33
38
  "preview": "vite preview",
34
39
  "type-check": "tsc --noEmit"
@@ -39,14 +44,31 @@
39
44
  "@types/react": "^19.2.5",
40
45
  "@types/react-dom": "^19.2.3",
41
46
  "@vitejs/plugin-react": "^5.1.1",
47
+ "autoprefixer": "^10.4.23",
48
+ "cross-env": "^10.1.0",
42
49
  "eslint": "^9.39.1",
43
50
  "eslint-plugin-react-hooks": "^7.0.1",
44
51
  "eslint-plugin-react-refresh": "^0.4.24",
45
52
  "globals": "^16.5.0",
53
+ "postcss": "^8.5.6",
54
+ "tailwindcss": "^3.4.19",
46
55
  "terser": "^5.44.1",
56
+ "tsup": "^8.5.1",
47
57
  "typescript": "~5.9.3",
48
58
  "typescript-eslint": "^8.46.4",
49
59
  "vite": "^7.2.4",
50
60
  "vite-plugin-dts": "^4.3.0"
61
+ },
62
+ "dependencies": {
63
+ "@mediapipe/camera_utils": "^0.3.1675466862",
64
+ "@mediapipe/drawing_utils": "^0.3.1675466124",
65
+ "@mediapipe/face_detection": "^0.4.1646425229",
66
+ "@mediapipe/face_mesh": "^0.4.1633559619",
67
+ "qrcode.react": "^4.2.0",
68
+ "react-router-dom": "^7.11.0"
69
+ },
70
+ "peerDependencies": {
71
+ "react": "^18.0.0 || ^19.0.0",
72
+ "react-dom": "^18.0.0 || ^19.0.0"
51
73
  }
52
74
  }
package/src/App.tsx CHANGED
@@ -1,13 +1,17 @@
1
-
2
- import './App.css'
1
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
2
+ import QRCodePage from './pages/QRCodePage';
3
+ import MobileRoute from './pages/MobileRoute';
3
4
 
4
5
  function App() {
5
-
6
6
  return (
7
- <>
8
- <h1>Hello World</h1>
9
- </>
10
- )
7
+ <BrowserRouter>
8
+ <Routes>
9
+ <Route path="/" element={<Navigate to="/qr" replace />} />
10
+ <Route path="/qr" element={<QRCodePage />} />
11
+ <Route path="/mobileroute" element={<MobileRoute />} />
12
+ </Routes>
13
+ </BrowserRouter>
14
+ );
11
15
  }
12
16
 
13
- export default App
17
+ export default App;
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { KycProvider } from '../contexts/KycContext';
3
+ import MobileRoute from '../pages/MobileRoute';
4
+ import QRCodePage from '../pages/QRCodePage';
5
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
6
+
7
+ export interface KycFlowProps {
8
+ apiBaseUrl: string;
9
+ sessionId: string;
10
+ serverKey: string;
11
+ deviceType?: string;
12
+ startAtQr?: boolean;
13
+ onClose?: () => void;
14
+ }
15
+
16
+ export const KycFlow: React.FC<KycFlowProps> = ({
17
+ apiBaseUrl,
18
+ sessionId,
19
+ serverKey,
20
+ deviceType,
21
+ startAtQr = true,
22
+ onClose,
23
+ }) => {
24
+ return (
25
+ <KycProvider
26
+ apiBaseUrl={apiBaseUrl}
27
+ sessionId={sessionId}
28
+ serverKey={serverKey}
29
+ deviceType={deviceType}
30
+ >
31
+ <BrowserRouter>
32
+ <Routes>
33
+ <Route path="/" element={<Navigate to={startAtQr ? "/qr" : "/mobileroute"} replace />} />
34
+ <Route path="/qr" element={<QRCodePage onClose={onClose} />} />
35
+ <Route path="/mobileroute" element={<MobileRoute onClose={onClose} />} />
36
+ </Routes>
37
+ </BrowserRouter>
38
+ </KycProvider>
39
+ );
40
+ };
41
+
@@ -0,0 +1,3 @@
1
+ export { KycFlow } from './KycFlow';
2
+ export type { KycFlowProps } from './KycFlow';
3
+
@@ -0,0 +1,66 @@
1
+ import React, { createContext, useContext, ReactNode } from 'react';
2
+ import { KycApiService, type KycApiConfig } from '../services/kycApiService';
3
+
4
+ interface KycContextValue {
5
+ apiService: KycApiService | null;
6
+ setApiConfig: (config: KycApiConfig) => void;
7
+ }
8
+
9
+ const KycContext = createContext<KycContextValue>({
10
+ apiService: null,
11
+ setApiConfig: () => {},
12
+ });
13
+
14
+ export const useKycContext = () => {
15
+ const context = useContext(KycContext);
16
+ if (!context) {
17
+ throw new Error('useKycContext must be used within KycProvider');
18
+ }
19
+ return context;
20
+ };
21
+
22
+ interface KycProviderProps {
23
+ children: ReactNode;
24
+ apiBaseUrl: string;
25
+ sessionId: string;
26
+ serverKey: string;
27
+ deviceType?: string;
28
+ }
29
+
30
+ export const KycProvider: React.FC<KycProviderProps> = ({
31
+ children,
32
+ apiBaseUrl,
33
+ sessionId,
34
+ serverKey,
35
+ deviceType,
36
+ }) => {
37
+ const [apiService, setApiService] = React.useState<KycApiService | null>(null);
38
+
39
+ React.useEffect(() => {
40
+ if (apiBaseUrl && sessionId && serverKey) {
41
+ const service = new KycApiService({
42
+ apiBaseUrl,
43
+ sessionId,
44
+ serverKey,
45
+ deviceType,
46
+ });
47
+ setApiService(service);
48
+ }
49
+ }, [apiBaseUrl, sessionId, serverKey, deviceType]);
50
+
51
+ const setApiConfig = React.useCallback((config: KycApiConfig) => {
52
+ if (apiService) {
53
+ apiService.updateConfig(config);
54
+ } else {
55
+ const service = new KycApiService(config);
56
+ setApiService(service);
57
+ }
58
+ }, [apiService]);
59
+
60
+ return (
61
+ <KycContext.Provider value={{ apiService, setApiConfig }}>
62
+ {children}
63
+ </KycContext.Provider>
64
+ );
65
+ };
66
+
@@ -0,0 +1,226 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import type { DocumentUploadCallbacks } from '../types';
3
+ import type { DocumentUploadState } from '../types';
4
+
5
+ export interface DocumentUploadHookCallbacks extends DocumentUploadCallbacks {
6
+ onDocumentUpload?: (blob: Blob, docType: string) => Promise<void>;
7
+ }
8
+
9
+ export function useDocumentUpload(callbacks?: DocumentUploadHookCallbacks) {
10
+ const [state, setState] = useState<DocumentUploadState>({
11
+ docType: 'CNIC',
12
+ isDocScanMode: false,
13
+ docPreviewUrl: null,
14
+ docFileName: '',
15
+ loading: false,
16
+ });
17
+
18
+ const fileInputRef = useRef<HTMLInputElement>(null);
19
+ const docVideoRef = useRef<HTMLVideoElement>(null);
20
+ const docStreamRef = useRef<MediaStream | null>(null);
21
+ const docPendingBlobRef = useRef<Blob | null>(null);
22
+
23
+ const getRearStream = async (): Promise<MediaStream> => {
24
+ const attempts: MediaStreamConstraints[] = [
25
+ { video: { facingMode: { exact: 'environment' }, width: { ideal: 1280 }, height: { ideal: 1920 } }, audio: false } as MediaStreamConstraints,
26
+ { video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 1920 } }, audio: false } as unknown as MediaStreamConstraints,
27
+ { video: { facingMode: { exact: 'environment' }, width: { ideal: 720 }, height: { ideal: 1280 } }, audio: false } as MediaStreamConstraints,
28
+ { video: { facingMode: 'environment', width: { ideal: 720 }, height: { ideal: 1280 } }, audio: false } as unknown as MediaStreamConstraints,
29
+ ];
30
+
31
+ for (const c of attempts) {
32
+ try { return await navigator.mediaDevices.getUserMedia(c); } catch { /* continue */ }
33
+ }
34
+
35
+ let temp: MediaStream | null = null;
36
+ try {
37
+ temp = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
38
+ } catch (e) {
39
+ throw e;
40
+ } finally {
41
+ try { temp?.getTracks().forEach(t => t.stop()); } catch {}
42
+ }
43
+
44
+ const devices = await navigator.mediaDevices.enumerateDevices();
45
+ const videoInputs = devices.filter(d => d.kind === 'videoinput');
46
+ const rear = videoInputs.find(d => /back|rear|environment/i.test(d.label));
47
+ const chosen = rear || videoInputs.find(d => !/front|user|face/i.test(d.label)) || videoInputs[0];
48
+ if (!chosen) throw new Error('No video input devices found');
49
+
50
+ const byDeviceAttempts: MediaStreamConstraints[] = [
51
+ { video: { deviceId: { exact: chosen.deviceId }, width: { ideal: 1280 }, height: { ideal: 1920 } }, audio: false },
52
+ { video: { deviceId: { exact: chosen.deviceId }, width: { ideal: 720 }, height: { ideal: 1280 } }, audio: false },
53
+ { video: { deviceId: { exact: chosen.deviceId } }, audio: false } as MediaStreamConstraints,
54
+ ];
55
+ for (const c of byDeviceAttempts) {
56
+ try { return await navigator.mediaDevices.getUserMedia(c); } catch { /* continue */ }
57
+ }
58
+
59
+ return await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
60
+ };
61
+
62
+ const startDocCamera = async () => {
63
+ try {
64
+ if (docStreamRef.current) {
65
+ try { docStreamRef.current.getTracks().forEach(t => t.stop()); } catch {}
66
+ docStreamRef.current = null;
67
+ }
68
+ const stream = await getRearStream();
69
+ if (docVideoRef.current) {
70
+ docVideoRef.current.srcObject = stream;
71
+ await docVideoRef.current.play().catch(() => {});
72
+ }
73
+ docStreamRef.current = stream;
74
+ } catch (e: any) {
75
+ console.error('Could not start document camera:', e);
76
+ setState(prev => ({ ...prev, isDocScanMode: false }));
77
+ if (callbacks?.onError) {
78
+ callbacks.onError(e);
79
+ }
80
+ }
81
+ };
82
+
83
+ const handleDocumentUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
84
+ if (state.loading) return;
85
+ const file = event.target.files?.[0];
86
+ if (!file) return;
87
+ setState(prev => ({ ...prev, docFileName: file.name || '' }));
88
+ try {
89
+ const objectUrl = URL.createObjectURL(file);
90
+ setState(prev => ({ ...prev, docPreviewUrl: objectUrl }));
91
+ docPendingBlobRef.current = file;
92
+ } catch (err: any) {
93
+ console.error('Could not preview document:', err);
94
+ if (callbacks?.onError) {
95
+ callbacks.onError(err);
96
+ }
97
+ }
98
+ };
99
+
100
+ const handleConfirmDocumentUpload = async () => {
101
+ if (state.loading || !docPendingBlobRef.current) return;
102
+ setState(prev => ({ ...prev, loading: true }));
103
+ try {
104
+ const file = docPendingBlobRef.current as File;
105
+
106
+ // Upload document if callback provided
107
+ if (callbacks?.onDocumentUpload) {
108
+ try {
109
+ await callbacks.onDocumentUpload(file, state.docType);
110
+ } catch (uploadError: any) {
111
+ throw new Error(uploadError.message || 'Failed to upload document');
112
+ }
113
+ }
114
+
115
+ if (callbacks?.onUpload) {
116
+ callbacks.onUpload(file, state.docType);
117
+ }
118
+
119
+ setState(prev => ({
120
+ ...prev,
121
+ docPreviewUrl: null,
122
+ docFileName: '',
123
+ loading: false,
124
+ }));
125
+ docPendingBlobRef.current = null;
126
+ } catch (err: any) {
127
+ console.error('Document upload failed:', err);
128
+ setState(prev => ({ ...prev, loading: false }));
129
+ if (callbacks?.onError) {
130
+ callbacks.onError(err);
131
+ }
132
+ }
133
+ };
134
+
135
+ const handleManualCapture = async () => {
136
+ if (!docVideoRef.current || state.loading) return;
137
+
138
+ setState(prev => ({ ...prev, loading: true }));
139
+ try {
140
+ const video = docVideoRef.current;
141
+
142
+ // Create canvas and capture current frame
143
+ const canvas = document.createElement('canvas');
144
+ const width = video.videoWidth || 1280;
145
+ const height = video.videoHeight || 720;
146
+ canvas.width = width;
147
+ canvas.height = height;
148
+
149
+ const ctx = canvas.getContext('2d');
150
+ if (!ctx) throw new Error('Canvas not supported');
151
+
152
+ // Draw video frame to canvas
153
+ ctx.drawImage(video, 0, 0, width, height);
154
+
155
+ // Convert to blob
156
+ const blob = await new Promise<Blob>((resolve) => {
157
+ canvas.toBlob((blob: Blob | null) => {
158
+ resolve(blob || new Blob());
159
+ }, 'image/jpeg', 0.92);
160
+ });
161
+
162
+ const file = new File([blob], 'document.jpg', { type: 'image/jpeg' });
163
+
164
+ // Upload document if callback provided
165
+ if (callbacks?.onDocumentUpload) {
166
+ try {
167
+ await callbacks.onDocumentUpload(file, state.docType);
168
+ } catch (uploadError: any) {
169
+ throw new Error(uploadError.message || 'Failed to upload document');
170
+ }
171
+ }
172
+
173
+ // Create preview URL
174
+ const objectUrl = URL.createObjectURL(file);
175
+ setState(prev => ({
176
+ ...prev,
177
+ docPreviewUrl: objectUrl,
178
+ isDocScanMode: false,
179
+ loading: false,
180
+ }));
181
+
182
+ docPendingBlobRef.current = file;
183
+
184
+ // Stop camera
185
+ if (docStreamRef.current) {
186
+ docStreamRef.current.getTracks().forEach((t) => t.stop());
187
+ docStreamRef.current = null;
188
+ }
189
+
190
+ if (callbacks?.onScan) {
191
+ callbacks.onScan(file, state.docType);
192
+ }
193
+ } catch (err: any) {
194
+ console.error('Capture failed:', err);
195
+ setState(prev => ({ ...prev, loading: false }));
196
+ if (callbacks?.onError) {
197
+ callbacks.onError(err);
198
+ }
199
+ }
200
+ };
201
+
202
+ useEffect(() => {
203
+ if (state.isDocScanMode) {
204
+ startDocCamera();
205
+ }
206
+
207
+ return () => {
208
+ if (docStreamRef.current) {
209
+ docStreamRef.current.getTracks().forEach((t) => t.stop());
210
+ docStreamRef.current = null;
211
+ }
212
+ };
213
+ }, [state.isDocScanMode]);
214
+
215
+ return {
216
+ state,
217
+ setState,
218
+ fileInputRef,
219
+ docVideoRef,
220
+ handleDocumentUpload,
221
+ handleConfirmDocumentUpload,
222
+ handleManualCapture,
223
+ startDocCamera,
224
+ };
225
+ }
226
+
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export { useDocumentUpload } from './hooks/useDocumentUpload';
3
+
@@ -0,0 +1,16 @@
1
+ export type DocumentType = 'CNIC' | 'Passport' | 'DrivingLicense';
2
+
3
+ export interface DocumentUploadState {
4
+ docType: DocumentType;
5
+ isDocScanMode: boolean;
6
+ docPreviewUrl: string | null;
7
+ docFileName: string;
8
+ loading: boolean;
9
+ }
10
+
11
+ export interface DocumentUploadCallbacks {
12
+ onUpload?: (file: File, docType: DocumentType) => void;
13
+ onScan?: (file: File, docType: DocumentType) => void;
14
+ onError?: (error: Error) => void;
15
+ }
16
+
@@ -0,0 +1,62 @@
1
+ import { useRef, useState, useEffect } from 'react';
2
+
3
+ export function useCamera() {
4
+ const videoRef = useRef<HTMLVideoElement>(null);
5
+ const streamRef = useRef<MediaStream | null>(null);
6
+ const [cameraReady, setCameraReady] = useState(false);
7
+
8
+ const startCamera = async () => {
9
+ try {
10
+ const stream = await navigator.mediaDevices.getUserMedia({
11
+ video: {
12
+ facingMode: "user",
13
+ width: { ideal: 1280 },
14
+ height: { ideal: 720 }
15
+ },
16
+ audio: false
17
+ });
18
+ if (videoRef.current) {
19
+ videoRef.current.srcObject = stream;
20
+ await videoRef.current.play().catch(() => { });
21
+ requestAnimationFrame(() => {
22
+ try {
23
+ const v = videoRef.current!;
24
+ // Canvas will be synced separately
25
+ } catch { }
26
+ });
27
+ }
28
+ streamRef.current = stream;
29
+ setCameraReady(true);
30
+ } catch (e: any) {
31
+ console.error('Camera access error:', e);
32
+ setCameraReady(false);
33
+ throw e;
34
+ }
35
+ };
36
+
37
+ const stopCamera = () => {
38
+ if (streamRef.current) {
39
+ streamRef.current.getTracks().forEach((t) => t.stop());
40
+ streamRef.current = null;
41
+ }
42
+ if (videoRef.current) {
43
+ videoRef.current.srcObject = null;
44
+ }
45
+ setCameraReady(false);
46
+ };
47
+
48
+ useEffect(() => {
49
+ startCamera();
50
+ return () => {
51
+ stopCamera();
52
+ };
53
+ }, []);
54
+
55
+ return {
56
+ videoRef,
57
+ cameraReady,
58
+ startCamera,
59
+ stopCamera,
60
+ };
61
+ }
62
+