@casperid/react 1.0.1 → 2.0.0
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/README.md +40 -29
- package/dist/index.cjs +1 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +217 -3524
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -26
- package/dist/style.css +0 -1
- package/src/CasperIDModal.tsx +0 -777
- package/src/index.css +0 -217
- package/src/index.ts +0 -41
- package/src/screens/AuthSelection.tsx +0 -221
- package/src/screens/DocumentScan.tsx +0 -348
- package/src/screens/FaceScan.tsx +0 -368
- package/src/screens/IdentityVerified.tsx +0 -51
- package/src/screens/L1Setup.tsx +0 -335
- package/src/screens/Login.tsx +0 -186
- package/src/screens/MintingIdentity.tsx +0 -446
- package/src/screens/PasskeyAuth.tsx +0 -259
- package/src/screens/PasskeyRegister.tsx +0 -281
- package/src/screens/PermissionsRequest.tsx +0 -96
- package/src/screens/PinVerification.tsx +0 -321
- package/src/screens/ReviewData.tsx +0 -315
- package/src/screens/SecurityUpgrade.tsx +0 -83
- package/src/screens/VerifyIdentityChoice.tsx +0 -59
- package/src/shared/Footer.tsx +0 -13
- package/src/shared/GlassContainer.tsx +0 -17
- package/src/shared/Header.tsx +0 -62
- package/src/types.ts +0 -342
- package/src/utils/fetchWithTimeout.ts +0 -52
- package/src/utils/i18n.ts +0 -640
- package/src/utils/webauthn.ts +0 -261
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useState, useEffect } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { Camera, HelpCircle, Flashlight, Loader2, AlertCircle, RotateCcw } from 'lucide-react';
|
|
4
|
-
import type { DocumentType, DocumentUploadResponse, ExtractedDocumentData } from '../types';
|
|
5
|
-
|
|
6
|
-
import { t } from '../utils/i18n';
|
|
7
|
-
|
|
8
|
-
interface DocumentScanProps {
|
|
9
|
-
requestId: string;
|
|
10
|
-
documentType: DocumentType;
|
|
11
|
-
onNext: (extractedData: ExtractedDocumentData, docUploadId: string) => void;
|
|
12
|
-
onError?: (error: string) => void;
|
|
13
|
-
apiBaseUrl?: string;
|
|
14
|
-
previewMode?: boolean;
|
|
15
|
-
language?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const getDocumentTypeLabels = (language: string) => ({
|
|
19
|
-
passport: t('passport', language),
|
|
20
|
-
drivers_license: t('drivers_license', language),
|
|
21
|
-
national_id: t('national_id', language)
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const DocumentScan: React.FC<DocumentScanProps> = ({
|
|
25
|
-
requestId,
|
|
26
|
-
documentType,
|
|
27
|
-
onNext,
|
|
28
|
-
onError,
|
|
29
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
30
|
-
previewMode = false,
|
|
31
|
-
language = 'EN'
|
|
32
|
-
}) => {
|
|
33
|
-
const videoRef = useRef<HTMLVideoElement>(null);
|
|
34
|
-
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
35
|
-
const streamRef = useRef<MediaStream | null>(null);
|
|
36
|
-
|
|
37
|
-
const [cameraReady, setCameraReady] = useState(false);
|
|
38
|
-
const [capturedImage, setCapturedImage] = useState<Blob | null>(null);
|
|
39
|
-
const [capturedImageUrl, setCapturedImageUrl] = useState<string | null>(null);
|
|
40
|
-
const [isUploading, setIsUploading] = useState(false);
|
|
41
|
-
const [error, setError] = useState('');
|
|
42
|
-
const [scanSide, setScanSide] = useState<'front' | 'back'>('front');
|
|
43
|
-
const [frontImage, setFrontImage] = useState<Blob | null>(null);
|
|
44
|
-
|
|
45
|
-
// Initialize camera
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const initCamera = async () => {
|
|
48
|
-
try {
|
|
49
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
50
|
-
video: {
|
|
51
|
-
facingMode: 'environment', // Use back camera for documents
|
|
52
|
-
width: { ideal: 1280 },
|
|
53
|
-
height: { ideal: 720 }
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
streamRef.current = stream;
|
|
57
|
-
if (videoRef.current) {
|
|
58
|
-
videoRef.current.srcObject = stream;
|
|
59
|
-
videoRef.current.onloadedmetadata = () => {
|
|
60
|
-
setCameraReady(true);
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
} catch (err: any) {
|
|
64
|
-
// Fallback to front camera
|
|
65
|
-
try {
|
|
66
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
67
|
-
video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } }
|
|
68
|
-
});
|
|
69
|
-
streamRef.current = stream;
|
|
70
|
-
if (videoRef.current) {
|
|
71
|
-
videoRef.current.srcObject = stream;
|
|
72
|
-
videoRef.current.onloadedmetadata = () => {
|
|
73
|
-
setCameraReady(true);
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
} catch (fallbackErr) {
|
|
77
|
-
setError(t('unable_access_camera', language));
|
|
78
|
-
onError?.('Camera access failed');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
initCamera();
|
|
84
|
-
|
|
85
|
-
return () => {
|
|
86
|
-
if (streamRef.current) {
|
|
87
|
-
streamRef.current.getTracks().forEach(track => track.stop());
|
|
88
|
-
}
|
|
89
|
-
if (capturedImageUrl) {
|
|
90
|
-
URL.revokeObjectURL(capturedImageUrl);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
}, [onError]);
|
|
94
|
-
|
|
95
|
-
// Capture photo
|
|
96
|
-
const handleCapture = async () => {
|
|
97
|
-
if (!videoRef.current || !canvasRef.current) return;
|
|
98
|
-
|
|
99
|
-
const canvas = canvasRef.current;
|
|
100
|
-
const video = videoRef.current;
|
|
101
|
-
const ctx = canvas.getContext('2d');
|
|
102
|
-
|
|
103
|
-
if (!ctx) return;
|
|
104
|
-
|
|
105
|
-
canvas.width = video.videoWidth;
|
|
106
|
-
canvas.height = video.videoHeight;
|
|
107
|
-
ctx.drawImage(video, 0, 0);
|
|
108
|
-
|
|
109
|
-
canvas.toBlob((blob) => {
|
|
110
|
-
if (blob) {
|
|
111
|
-
setCapturedImage(blob);
|
|
112
|
-
setCapturedImageUrl(URL.createObjectURL(blob));
|
|
113
|
-
}
|
|
114
|
-
}, 'image/jpeg', 0.9);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Retake photo
|
|
118
|
-
const handleRetake = () => {
|
|
119
|
-
if (capturedImageUrl) {
|
|
120
|
-
URL.revokeObjectURL(capturedImageUrl);
|
|
121
|
-
}
|
|
122
|
-
setCapturedImage(null);
|
|
123
|
-
setCapturedImageUrl(null);
|
|
124
|
-
setError('');
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Confirm and proceed
|
|
128
|
-
const handleConfirm = async () => {
|
|
129
|
-
setIsUploading(true);
|
|
130
|
-
|
|
131
|
-
if (previewMode) {
|
|
132
|
-
setTimeout(() => {
|
|
133
|
-
setIsUploading(false);
|
|
134
|
-
onNext({
|
|
135
|
-
first_name: 'JOHN',
|
|
136
|
-
last_name: 'DOE',
|
|
137
|
-
date_of_birth: '01 JAN 1990',
|
|
138
|
-
nationality: 'Nigerian'
|
|
139
|
-
}, 'mock-upload-id-789');
|
|
140
|
-
}, 2000);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!capturedImage) {
|
|
145
|
-
setIsUploading(false); // Ensure loading state is reset if no image
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (scanSide === 'front' && documentType !== 'passport') {
|
|
150
|
-
// For non-passport documents, need back side too
|
|
151
|
-
setFrontImage(capturedImage);
|
|
152
|
-
setCapturedImage(null);
|
|
153
|
-
setCapturedImageUrl(null);
|
|
154
|
-
setScanSide('back');
|
|
155
|
-
setIsUploading(false); // Reset loading state after setting side
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Upload document(s)
|
|
160
|
-
setIsUploading(true);
|
|
161
|
-
setError('');
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const formData = new FormData();
|
|
165
|
-
formData.append('document_type', documentType);
|
|
166
|
-
|
|
167
|
-
if (frontImage) {
|
|
168
|
-
formData.append('front_image', frontImage, 'front.jpg');
|
|
169
|
-
formData.append('back_image', capturedImage, 'back.jpg');
|
|
170
|
-
} else {
|
|
171
|
-
formData.append('front_image', capturedImage, 'document.jpg');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const response = await fetch(`${apiBaseUrl}/api/v2/verification/layer3/documents/upload`, {
|
|
175
|
-
method: 'POST',
|
|
176
|
-
body: formData,
|
|
177
|
-
credentials: 'include',
|
|
178
|
-
headers: {
|
|
179
|
-
'X-Request-Id': requestId
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (!response.ok) {
|
|
184
|
-
const errorData = await response.json().catch(() => ({}));
|
|
185
|
-
throw new Error(errorData.error || `Document upload failed with status ${response.status}`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const data: DocumentUploadResponse = await response.json();
|
|
189
|
-
|
|
190
|
-
if (data.success && data.extracted_data) {
|
|
191
|
-
// Stop camera
|
|
192
|
-
if (streamRef.current) {
|
|
193
|
-
streamRef.current.getTracks().forEach(track => track.stop());
|
|
194
|
-
}
|
|
195
|
-
onNext(data.extracted_data, data.upload_id || '');
|
|
196
|
-
} else {
|
|
197
|
-
setError(data.error || 'Failed to process document. Please try again.');
|
|
198
|
-
setIsUploading(false);
|
|
199
|
-
}
|
|
200
|
-
} catch (err) {
|
|
201
|
-
setError('Upload failed. Please try again.');
|
|
202
|
-
setIsUploading(false);
|
|
203
|
-
onError?.('Document upload failed');
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<motion.div
|
|
209
|
-
initial={{ opacity: 0 }}
|
|
210
|
-
animate={{ opacity: 1 }}
|
|
211
|
-
exit={{ opacity: 0 }}
|
|
212
|
-
className="flex-1 flex flex-col relative"
|
|
213
|
-
>
|
|
214
|
-
{/* Hidden canvas for capture */}
|
|
215
|
-
<canvas ref={canvasRef} className="hidden" />
|
|
216
|
-
|
|
217
|
-
{/* Simulated dark camera background */}
|
|
218
|
-
<div className="absolute inset-0 z-0">
|
|
219
|
-
<div className="w-full h-full bg-slate-800 flex items-center justify-center">
|
|
220
|
-
<div className="w-full h-full opacity-30 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-[#6DE8EC]/20 via-transparent to-transparent" />
|
|
221
|
-
</div>
|
|
222
|
-
<div className="absolute inset-0 bg-black/30 backdrop-blur-[2px]" />
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
{/* Top label */}
|
|
226
|
-
<div className="relative z-10 p-6 flex flex-col items-center">
|
|
227
|
-
<div className="text-center px-4 py-2 dark:bg-black/40 bg-white/40 backdrop-blur-xl rounded-full border dark:border-white/10 border-black/5">
|
|
228
|
-
<h3 className="text-main text-sm font-medium tracking-wide uppercase">
|
|
229
|
-
{t('scan_instruction', language)
|
|
230
|
-
.replace('{side}', t(scanSide, language))
|
|
231
|
-
.replace('{docType}', getDocumentTypeLabels(language)[documentType])}
|
|
232
|
-
</h3>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
|
|
236
|
-
{/* ID frame with video or captured image */}
|
|
237
|
-
<div className="relative z-10 flex-1 flex items-center justify-center px-6">
|
|
238
|
-
<div className="w-full aspect-[1.58/1] relative">
|
|
239
|
-
{/* Video or captured image */}
|
|
240
|
-
{capturedImageUrl ? (
|
|
241
|
-
<img
|
|
242
|
-
src={capturedImageUrl}
|
|
243
|
-
alt="Captured document"
|
|
244
|
-
className="absolute inset-0 w-full h-full object-cover rounded-2xl"
|
|
245
|
-
/>
|
|
246
|
-
) : (
|
|
247
|
-
<video
|
|
248
|
-
ref={videoRef}
|
|
249
|
-
autoPlay
|
|
250
|
-
playsInline
|
|
251
|
-
muted
|
|
252
|
-
className="absolute inset-0 w-full h-full object-cover rounded-2xl"
|
|
253
|
-
/>
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
{/* Frame overlay */}
|
|
257
|
-
<div className="absolute inset-0 border border-[#6DE8EC] rounded-2xl shadow-[0_0_20px_rgba(109,232,236,0.3),inset_0_0_15px_rgba(109,232,236,0.2)]" />
|
|
258
|
-
<div className="absolute top-0 left-0 w-8 h-8 border-t-4 border-l-4 border-[#6DE8EC] rounded-tl-2xl" />
|
|
259
|
-
<div className="absolute top-0 right-0 w-8 h-8 border-t-4 border-r-4 border-[#6DE8EC] rounded-tr-2xl" />
|
|
260
|
-
<div className="absolute bottom-0 left-0 w-8 h-8 border-b-4 border-l-4 border-[#6DE8EC] rounded-bl-2xl" />
|
|
261
|
-
<div className="absolute bottom-0 right-0 w-8 h-8 border-b-4 border-r-4 border-[#6DE8EC] rounded-br-2xl" />
|
|
262
|
-
|
|
263
|
-
{/* Scanning line (only when video is active) */}
|
|
264
|
-
{!capturedImageUrl && cameraReady && (
|
|
265
|
-
<motion.div
|
|
266
|
-
animate={{ top: ['0%', '100%', '0%'] }}
|
|
267
|
-
transition={{ duration: 4, repeat: Infinity, ease: 'linear' }}
|
|
268
|
-
className="absolute left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-[#6DE8EC] to-transparent opacity-50"
|
|
269
|
-
/>
|
|
270
|
-
)}
|
|
271
|
-
|
|
272
|
-
{/* Loading overlay */}
|
|
273
|
-
{!cameraReady && !capturedImageUrl && (
|
|
274
|
-
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-2xl">
|
|
275
|
-
<Loader2 className="w-8 h-8 text-[#6DE8EC] animate-spin" />
|
|
276
|
-
</div>
|
|
277
|
-
)}
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
{/* Error message */}
|
|
282
|
-
{error && (
|
|
283
|
-
<div className="relative z-10 mx-6 mb-4 p-4 bg-red-500/20 border border-red-500/30 rounded-xl flex items-center gap-3">
|
|
284
|
-
<AlertCircle className="w-5 h-5 text-red-500 shrink-0" />
|
|
285
|
-
<p className="text-red-400 text-sm font-medium">{error}</p>
|
|
286
|
-
</div>
|
|
287
|
-
)}
|
|
288
|
-
|
|
289
|
-
{/* Bottom controls */}
|
|
290
|
-
<div className="relative z-10 p-8 dark:bg-black/60 bg-white/60 backdrop-blur-2xl border-t dark:border-white/10 border-black/5 rounded-t-3xl">
|
|
291
|
-
<p className="text-muted text-xs text-center mb-6 font-medium">
|
|
292
|
-
{capturedImageUrl
|
|
293
|
-
? t('review_capture', language)
|
|
294
|
-
: t('align_document', language)}
|
|
295
|
-
</p>
|
|
296
|
-
|
|
297
|
-
<div className="flex items-center justify-between gap-4">
|
|
298
|
-
{capturedImageUrl ? (
|
|
299
|
-
<>
|
|
300
|
-
<button
|
|
301
|
-
onClick={handleRetake}
|
|
302
|
-
disabled={isUploading}
|
|
303
|
-
className="w-12 h-12 rounded-xl dark:bg-white/5 bg-black/5 flex items-center justify-center text-muted disabled:opacity-50"
|
|
304
|
-
>
|
|
305
|
-
<RotateCcw className="w-6 h-6" />
|
|
306
|
-
</button>
|
|
307
|
-
<button
|
|
308
|
-
onClick={handleConfirm}
|
|
309
|
-
disabled={isUploading}
|
|
310
|
-
className="flex-1 bg-brand hover:bg-brand/90 text-white h-14 rounded-2xl flex items-center justify-center gap-2 font-bold text-lg transition-all active:scale-95 shadow-[0_0_25px_rgba(130,66,240,0.5)] disabled:opacity-70"
|
|
311
|
-
>
|
|
312
|
-
{isUploading ? (
|
|
313
|
-
<>
|
|
314
|
-
<Loader2 className="w-6 h-6 animate-spin" />
|
|
315
|
-
<span>{t('processing', language)}</span>
|
|
316
|
-
</>
|
|
317
|
-
) : (
|
|
318
|
-
<span>{scanSide === 'front' && documentType !== 'passport' ? t('next_back_side', language) : t('confirm', language)}</span>
|
|
319
|
-
)}
|
|
320
|
-
</button>
|
|
321
|
-
<div className="w-12 h-12" /> {/* Spacer for alignment */}
|
|
322
|
-
</>
|
|
323
|
-
) : (
|
|
324
|
-
<>
|
|
325
|
-
<button className="w-12 h-12 rounded-xl dark:bg-white/5 bg-black/5 flex items-center justify-center text-muted">
|
|
326
|
-
<Flashlight className="w-6 h-6" />
|
|
327
|
-
</button>
|
|
328
|
-
<button
|
|
329
|
-
onClick={handleCapture}
|
|
330
|
-
disabled={!cameraReady}
|
|
331
|
-
className="flex-1 bg-brand hover:bg-brand/90 text-white h-14 rounded-2xl flex items-center justify-center gap-2 font-bold text-lg transition-all active:scale-95 shadow-[0_0_25px_rgba(130,66,240,0.5)] disabled:opacity-50"
|
|
332
|
-
>
|
|
333
|
-
<Camera className="w-6 h-6" />
|
|
334
|
-
<span>{t('capture', language)}</span>
|
|
335
|
-
</button>
|
|
336
|
-
<button className="w-12 h-12 rounded-xl dark:bg-white/5 bg-black/5 flex items-center justify-center text-muted">
|
|
337
|
-
<HelpCircle className="w-6 h-6" />
|
|
338
|
-
</button>
|
|
339
|
-
</>
|
|
340
|
-
)}
|
|
341
|
-
</div>
|
|
342
|
-
<div className="w-32 h-1.5 bg-slate-600/30 rounded-full mx-auto mt-8" />
|
|
343
|
-
</div>
|
|
344
|
-
</motion.div>
|
|
345
|
-
);
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
export default DocumentScan;
|