@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,321 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { Delete, ArrowRight, Loader2, RefreshCw } from 'lucide-react';
|
|
4
|
-
import type { VerifyOTPResponse } from '../types';
|
|
5
|
-
import { t } from '../utils/i18n';
|
|
6
|
-
|
|
7
|
-
interface UserAccountInfo {
|
|
8
|
-
user_id: string;
|
|
9
|
-
wallet?: string;
|
|
10
|
-
tier?: string;
|
|
11
|
-
did_address?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface PinVerificationProps {
|
|
15
|
-
email: string;
|
|
16
|
-
verificationId: string;
|
|
17
|
-
onNext: (sessionToken: string, userAccount?: UserAccountInfo | null, businessToken?: string) => void;
|
|
18
|
-
onError?: (error: string) => void;
|
|
19
|
-
apiBaseUrl?: string;
|
|
20
|
-
apiKey?: string;
|
|
21
|
-
previewMode?: boolean;
|
|
22
|
-
language?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const PinVerification: React.FC<PinVerificationProps> = ({
|
|
26
|
-
email,
|
|
27
|
-
verificationId,
|
|
28
|
-
onNext,
|
|
29
|
-
onError,
|
|
30
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
31
|
-
apiKey,
|
|
32
|
-
previewMode = false,
|
|
33
|
-
language = 'EN'
|
|
34
|
-
}) => {
|
|
35
|
-
const [pin, setPin] = useState<string[]>(['', '', '', '', '', '']);
|
|
36
|
-
const [isVerifying, setIsVerifying] = useState(false);
|
|
37
|
-
const [isResending, setIsResending] = useState(false);
|
|
38
|
-
const [error, setError] = useState('');
|
|
39
|
-
const [resendCooldown, setResendCooldown] = useState(0);
|
|
40
|
-
const [currentVerificationId, setCurrentVerificationId] = useState(verificationId);
|
|
41
|
-
|
|
42
|
-
// Cooldown timer for resend
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (resendCooldown > 0) {
|
|
45
|
-
const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);
|
|
46
|
-
return () => clearTimeout(timer);
|
|
47
|
-
}
|
|
48
|
-
}, [resendCooldown]);
|
|
49
|
-
|
|
50
|
-
// Auto-verify when all digits are entered
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (pin.every(digit => digit !== '') && !isVerifying) {
|
|
53
|
-
handleVerify();
|
|
54
|
-
}
|
|
55
|
-
}, [pin]);
|
|
56
|
-
|
|
57
|
-
const handleDigitPress = useCallback((digit: string) => {
|
|
58
|
-
setError('');
|
|
59
|
-
setPin(prev => {
|
|
60
|
-
const newPin = [...prev];
|
|
61
|
-
const emptyIndex = newPin.findIndex(d => d === '');
|
|
62
|
-
if (emptyIndex !== -1) {
|
|
63
|
-
newPin[emptyIndex] = digit;
|
|
64
|
-
}
|
|
65
|
-
return newPin;
|
|
66
|
-
});
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const handleDelete = useCallback(() => {
|
|
70
|
-
setError('');
|
|
71
|
-
setPin(prev => {
|
|
72
|
-
const newPin = [...prev];
|
|
73
|
-
// Find last filled digit
|
|
74
|
-
for (let i = newPin.length - 1; i >= 0; i--) {
|
|
75
|
-
if (newPin[i] !== '') {
|
|
76
|
-
newPin[i] = '';
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return newPin;
|
|
81
|
-
});
|
|
82
|
-
}, []);
|
|
83
|
-
|
|
84
|
-
// Keyboard input support
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
87
|
-
if (isVerifying) return;
|
|
88
|
-
|
|
89
|
-
// Handle number keys (both main keyboard and numpad)
|
|
90
|
-
if (/^[0-9]$/.test(e.key)) {
|
|
91
|
-
e.preventDefault();
|
|
92
|
-
handleDigitPress(e.key);
|
|
93
|
-
}
|
|
94
|
-
// Handle backspace/delete
|
|
95
|
-
else if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
handleDelete();
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
102
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
103
|
-
}, [isVerifying, handleDigitPress, handleDelete]);
|
|
104
|
-
|
|
105
|
-
const handleVerify = async () => {
|
|
106
|
-
const otp = pin.join('');
|
|
107
|
-
if (otp.length !== 6) {
|
|
108
|
-
setError(t('error_pin_incomplete', language));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
setIsVerifying(true);
|
|
113
|
-
setError('');
|
|
114
|
-
|
|
115
|
-
if (previewMode) {
|
|
116
|
-
setTimeout(() => {
|
|
117
|
-
setIsVerifying(false);
|
|
118
|
-
onNext('mock-session-token', null);
|
|
119
|
-
}, 1000);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const response = await fetch(`${apiBaseUrl}/api/auth/verify-otp`, {
|
|
125
|
-
method: 'POST',
|
|
126
|
-
headers: {
|
|
127
|
-
'Content-Type': 'application/json',
|
|
128
|
-
'X-App-ID': apiKey || ''
|
|
129
|
-
},
|
|
130
|
-
body: JSON.stringify({
|
|
131
|
-
verificationId: currentVerificationId,
|
|
132
|
-
otp
|
|
133
|
-
}),
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
const errorData = await response.json().catch(() => ({}));
|
|
138
|
-
throw new Error(errorData.error || `Verification failed with status ${response.status}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const data: VerifyOTPResponse = await response.json();
|
|
142
|
-
|
|
143
|
-
if (data.success && data.sessionToken) {
|
|
144
|
-
onNext(data.sessionToken, data.userAccount, data.business_token);
|
|
145
|
-
} else {
|
|
146
|
-
setError(data.error || t('invalid_code', language));
|
|
147
|
-
if (data.attemptsLeft !== undefined) {
|
|
148
|
-
setError(t('invalid_code_attempts', language).replace('{attempts}', data.attemptsLeft.toString()));
|
|
149
|
-
}
|
|
150
|
-
// Clear pin on error
|
|
151
|
-
setPin(['', '', '', '', '', '']);
|
|
152
|
-
onError?.(data.error || t('verification_failed', language));
|
|
153
|
-
}
|
|
154
|
-
} catch (err) {
|
|
155
|
-
const errorMessage = t('error_verify_failed', language);
|
|
156
|
-
setError(errorMessage);
|
|
157
|
-
setPin(['', '', '', '', '', '']);
|
|
158
|
-
onError?.(errorMessage);
|
|
159
|
-
} finally {
|
|
160
|
-
setIsVerifying(false);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const handleResend = async () => {
|
|
165
|
-
if (resendCooldown > 0 || isResending) return;
|
|
166
|
-
|
|
167
|
-
setIsResending(true);
|
|
168
|
-
setError('');
|
|
169
|
-
|
|
170
|
-
if (previewMode) {
|
|
171
|
-
setTimeout(() => {
|
|
172
|
-
setIsResending(false);
|
|
173
|
-
setResendCooldown(60);
|
|
174
|
-
setPin(['', '', '', '', '', '']);
|
|
175
|
-
}, 800);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const response = await fetch(`${apiBaseUrl}/api/auth/resend-otp`, {
|
|
181
|
-
method: 'POST',
|
|
182
|
-
headers: {
|
|
183
|
-
'Content-Type': 'application/json',
|
|
184
|
-
},
|
|
185
|
-
body: JSON.stringify({ verificationId: currentVerificationId }),
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
if (!response.ok) {
|
|
189
|
-
const errorData = await response.json().catch(() => ({}));
|
|
190
|
-
throw new Error(errorData.error || `Request failed with status ${response.status}`);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const data = await response.json();
|
|
194
|
-
|
|
195
|
-
if (data.success) {
|
|
196
|
-
setResendCooldown(60); // 60 second cooldown
|
|
197
|
-
setPin(['', '', '', '', '', '']);
|
|
198
|
-
// Note: verificationId stays the same for resend
|
|
199
|
-
} else {
|
|
200
|
-
setError(data.error || t('error_resend_failed', language));
|
|
201
|
-
}
|
|
202
|
-
} catch (err) {
|
|
203
|
-
setError(t('error_resend_failed', language));
|
|
204
|
-
} finally {
|
|
205
|
-
setIsResending(false);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// Mask email for display
|
|
210
|
-
const maskedEmail = email.replace(/(.{2})(.*)(@.*)/, '$1***$3');
|
|
211
|
-
|
|
212
|
-
return (
|
|
213
|
-
<motion.div
|
|
214
|
-
initial={{ opacity: 0, scale: 0.95 }}
|
|
215
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
216
|
-
exit={{ opacity: 0, scale: 1.05 }}
|
|
217
|
-
className="flex-1 flex flex-col px-8 pt-12"
|
|
218
|
-
>
|
|
219
|
-
<div className="space-y-2 mb-8">
|
|
220
|
-
<h1 className="text-3xl font-bold tracking-tight text-main">{t('verify_email', language)}</h1>
|
|
221
|
-
<p className="text-muted text-sm font-medium">
|
|
222
|
-
{t('enter_code', language)} <span className="text-main font-semibold">{maskedEmail}</span>
|
|
223
|
-
</p>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
{/* PIN Display */}
|
|
227
|
-
<div className="flex justify-between gap-2 mb-6">
|
|
228
|
-
{pin.map((digit, i) => (
|
|
229
|
-
<div
|
|
230
|
-
key={i}
|
|
231
|
-
className={`size-12 rounded-2xl dark:bg-black/20 bg-slate-100/50 flex items-center justify-center text-xl font-bold border transition-all ${digit
|
|
232
|
-
? 'text-brand dark:border-brand-30 border-brand-50'
|
|
233
|
-
: 'text-muted dark:border-white/10 border-slate-200'
|
|
234
|
-
} ${error ? 'border-red-400 dark:border-red-500' : ''}`}
|
|
235
|
-
>
|
|
236
|
-
{digit}
|
|
237
|
-
{!digit && pin.findIndex(d => d === '') === i && (
|
|
238
|
-
<div className="w-0.5 h-6 bg-brand animate-pulse" />
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
))}
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
{/* Error Message */}
|
|
245
|
-
{error && (
|
|
246
|
-
<p className="text-red-500 text-xs font-medium text-center mb-4">{error}</p>
|
|
247
|
-
)}
|
|
248
|
-
|
|
249
|
-
{/* Number Pad */}
|
|
250
|
-
<div className="grid grid-cols-3 gap-4 mb-8">
|
|
251
|
-
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
|
|
252
|
-
<button
|
|
253
|
-
key={num}
|
|
254
|
-
onClick={() => handleDigitPress(num.toString())}
|
|
255
|
-
disabled={isVerifying}
|
|
256
|
-
className="h-14 rounded-2xl dark:hover:bg-white/5 hover:bg-slate-100 flex items-center justify-center text-xl font-bold transition-colors text-main disabled:opacity-50"
|
|
257
|
-
>
|
|
258
|
-
{num}
|
|
259
|
-
</button>
|
|
260
|
-
))}
|
|
261
|
-
<div className="h-14" />
|
|
262
|
-
<button
|
|
263
|
-
onClick={() => handleDigitPress('0')}
|
|
264
|
-
disabled={isVerifying}
|
|
265
|
-
className="h-14 rounded-2xl dark:hover:bg-white/5 hover:bg-slate-100 flex items-center justify-center text-xl font-bold transition-colors text-main disabled:opacity-50"
|
|
266
|
-
>
|
|
267
|
-
0
|
|
268
|
-
</button>
|
|
269
|
-
<button
|
|
270
|
-
onClick={handleDelete}
|
|
271
|
-
disabled={isVerifying}
|
|
272
|
-
className="h-14 rounded-2xl dark:hover:bg-white/5 hover:bg-slate-100 flex items-center justify-center transition-colors text-main disabled:opacity-50"
|
|
273
|
-
>
|
|
274
|
-
<Delete className="w-6 h-6" />
|
|
275
|
-
</button>
|
|
276
|
-
</div>
|
|
277
|
-
|
|
278
|
-
{/* Action Buttons */}
|
|
279
|
-
<div className="mt-auto pb-8 space-y-4">
|
|
280
|
-
<button
|
|
281
|
-
onClick={handleVerify}
|
|
282
|
-
disabled={isVerifying || pin.some(d => d === '')}
|
|
283
|
-
className="w-full h-14 bg-brand hover:bg-brand/90 text-white font-bold rounded-2xl transition-all shadow-lg shadow-brand flex items-center justify-center gap-2 group disabled:opacity-50 disabled:cursor-not-allowed"
|
|
284
|
-
>
|
|
285
|
-
{isVerifying ? (
|
|
286
|
-
<>
|
|
287
|
-
<Loader2 className="w-5 h-5 animate-spin" />
|
|
288
|
-
<span>{t('verifying', language)}</span>
|
|
289
|
-
</>
|
|
290
|
-
) : (
|
|
291
|
-
<>
|
|
292
|
-
<span>{t('verify', language)}</span>
|
|
293
|
-
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
|
294
|
-
</>
|
|
295
|
-
)}
|
|
296
|
-
</button>
|
|
297
|
-
<button
|
|
298
|
-
onClick={handleResend}
|
|
299
|
-
disabled={resendCooldown > 0 || isResending}
|
|
300
|
-
className="w-full h-14 dark:bg-white/5 hover:dark:bg-white/10 bg-slate-100 hover:bg-slate-200 text-muted font-bold rounded-2xl transition-all border dark:border-white/10 border-slate-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
301
|
-
>
|
|
302
|
-
{isResending ? (
|
|
303
|
-
<>
|
|
304
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
305
|
-
<span>{t('sending', language)}</span>
|
|
306
|
-
</>
|
|
307
|
-
) : resendCooldown > 0 ? (
|
|
308
|
-
<span>{t('resend_code', language)} ({resendCooldown}s)</span>
|
|
309
|
-
) : (
|
|
310
|
-
<>
|
|
311
|
-
<RefreshCw className="w-4 h-4" />
|
|
312
|
-
<span>{t('resend_code', language)}</span>
|
|
313
|
-
</>
|
|
314
|
-
)}
|
|
315
|
-
</button>
|
|
316
|
-
</div>
|
|
317
|
-
</motion.div>
|
|
318
|
-
);
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
export default PinVerification;
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { User, CreditCard, Calendar, Globe, CheckCircle2, Sparkles, AlertCircle, Users } from 'lucide-react';
|
|
4
|
-
import type { ExtractedDocumentData, DocumentType, Layer3SubmitResponse } from '../types';
|
|
5
|
-
|
|
6
|
-
import { t } from '../utils/i18n';
|
|
7
|
-
|
|
8
|
-
interface ReviewDataProps {
|
|
9
|
-
requestId: string;
|
|
10
|
-
documentType: DocumentType;
|
|
11
|
-
extractedData: ExtractedDocumentData;
|
|
12
|
-
uploadId: string;
|
|
13
|
-
onNext: (confirmedData: ExtractedDocumentData) => void;
|
|
14
|
-
onRetake?: () => void;
|
|
15
|
-
onError?: (error: string) => void;
|
|
16
|
-
apiBaseUrl?: string;
|
|
17
|
-
previewMode?: boolean;
|
|
18
|
-
language?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const getDocumentTypeLabels = (language: string) => ({
|
|
22
|
-
passport: t('passport', language),
|
|
23
|
-
drivers_license: t('drivers_license', language),
|
|
24
|
-
national_id: t('national_id', language)
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const ReviewData: React.FC<ReviewDataProps> = ({
|
|
28
|
-
requestId,
|
|
29
|
-
documentType,
|
|
30
|
-
extractedData,
|
|
31
|
-
uploadId,
|
|
32
|
-
onNext,
|
|
33
|
-
onRetake,
|
|
34
|
-
onError,
|
|
35
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
36
|
-
previewMode = false,
|
|
37
|
-
language = 'EN'
|
|
38
|
-
}) => {
|
|
39
|
-
// Editable fields state - initialize with extracted data
|
|
40
|
-
const [formData, setFormData] = useState({
|
|
41
|
-
firstName: extractedData.first_name || '',
|
|
42
|
-
lastName: extractedData.last_name || '',
|
|
43
|
-
documentNumber: extractedData.document_number || '',
|
|
44
|
-
dateOfBirth: extractedData.date_of_birth || '',
|
|
45
|
-
nationality: extractedData.nationality || '',
|
|
46
|
-
gender: extractedData.gender || ''
|
|
47
|
-
});
|
|
48
|
-
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
49
|
-
const [error, setError] = useState('');
|
|
50
|
-
|
|
51
|
-
const handleInputChange = (field: keyof typeof formData, value: string) => {
|
|
52
|
-
setFormData(prev => ({ ...prev, [field]: value }));
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const handleConfirm = async () => {
|
|
56
|
-
// Validate required fields
|
|
57
|
-
if (!formData.firstName || !formData.lastName || !formData.documentNumber) {
|
|
58
|
-
setError(t('error_required_fields', language));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
setIsSubmitting(true);
|
|
63
|
-
setError('');
|
|
64
|
-
|
|
65
|
-
if (previewMode) {
|
|
66
|
-
setTimeout(() => {
|
|
67
|
-
setIsSubmitting(false);
|
|
68
|
-
onNext({
|
|
69
|
-
first_name: formData.firstName,
|
|
70
|
-
last_name: formData.lastName,
|
|
71
|
-
document_number: formData.documentNumber,
|
|
72
|
-
date_of_birth: formData.dateOfBirth,
|
|
73
|
-
nationality: formData.nationality,
|
|
74
|
-
gender: formData.gender,
|
|
75
|
-
document_type: documentType
|
|
76
|
-
});
|
|
77
|
-
}, 1500);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
// Submit confirmed data to the backend using /layer3/submit endpoint
|
|
83
|
-
const response = await fetch(`${apiBaseUrl}/api/v2/verification/layer3/submit`, {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: {
|
|
86
|
-
'Content-Type': 'application/json'
|
|
87
|
-
},
|
|
88
|
-
credentials: 'include',
|
|
89
|
-
body: JSON.stringify({
|
|
90
|
-
upload_id: uploadId,
|
|
91
|
-
liveness_verified: true,
|
|
92
|
-
document_data: {
|
|
93
|
-
first_name: formData.firstName,
|
|
94
|
-
last_name: formData.lastName,
|
|
95
|
-
document_number: formData.documentNumber,
|
|
96
|
-
date_of_birth: formData.dateOfBirth,
|
|
97
|
-
nationality: formData.nationality,
|
|
98
|
-
gender: formData.gender,
|
|
99
|
-
document_type: documentType
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (!response.ok) {
|
|
105
|
-
const errorData = await response.json().catch(() => ({}));
|
|
106
|
-
throw new Error(errorData.error || `Data submission failed with status ${response.status}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const data: Layer3SubmitResponse = await response.json();
|
|
110
|
-
|
|
111
|
-
if (data.success) {
|
|
112
|
-
// Pass confirmed data to parent
|
|
113
|
-
onNext({
|
|
114
|
-
first_name: formData.firstName,
|
|
115
|
-
last_name: formData.lastName,
|
|
116
|
-
document_number: formData.documentNumber,
|
|
117
|
-
date_of_birth: formData.dateOfBirth,
|
|
118
|
-
nationality: formData.nationality,
|
|
119
|
-
gender: formData.gender,
|
|
120
|
-
document_type: documentType
|
|
121
|
-
});
|
|
122
|
-
} else {
|
|
123
|
-
setError(data.error || 'Failed to submit data. Please try again.');
|
|
124
|
-
setIsSubmitting(false);
|
|
125
|
-
}
|
|
126
|
-
} catch (err) {
|
|
127
|
-
setError('Failed to submit data. Please try again.');
|
|
128
|
-
setIsSubmitting(false);
|
|
129
|
-
onError?.('Data submission failed');
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const confidence = extractedData.confidence || 0;
|
|
134
|
-
const confidencePercent = Math.round(confidence * 100);
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<motion.div
|
|
138
|
-
initial={{ opacity: 0, y: 20 }}
|
|
139
|
-
animate={{ opacity: 1, y: 0 }}
|
|
140
|
-
exit={{ opacity: 0, y: -20 }}
|
|
141
|
-
className="flex-1 overflow-y-auto px-6 pb-6"
|
|
142
|
-
>
|
|
143
|
-
{/* Progress bar */}
|
|
144
|
-
<div className="flex gap-1 mb-6">
|
|
145
|
-
{[1, 2, 3, 0].map((fill, i) => (
|
|
146
|
-
<div
|
|
147
|
-
key={i}
|
|
148
|
-
className={`h-1 flex-1 rounded-xl ${fill ? 'bg-brand' : 'bg-brand/20'}`}
|
|
149
|
-
/>
|
|
150
|
-
))}
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<div className="mb-6">
|
|
154
|
-
<h1 className="text-2xl font-bold text-main mb-2">{t('review_data', language)}</h1>
|
|
155
|
-
<p className="text-muted text-sm leading-relaxed">
|
|
156
|
-
{t('verify_data_desc', language)}
|
|
157
|
-
</p>
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
|
-
{/* Confidence indicator */}
|
|
161
|
-
{confidence > 0 && (
|
|
162
|
-
<div className="mb-6 p-3 rounded-xl dark:bg-white/5 bg-black/5 border dark:border-white/10 border-black/5">
|
|
163
|
-
<div className="flex items-center justify-between mb-2">
|
|
164
|
-
<span className="text-xs font-semibold text-muted">{t('ocr_confidence', language)}</span>
|
|
165
|
-
<span className={`text-xs font-bold ${confidencePercent >= 80 ? 'text-green-500' : confidencePercent >= 60 ? 'text-yellow-500' : 'text-red-500'}`}>
|
|
166
|
-
{confidencePercent}%
|
|
167
|
-
</span>
|
|
168
|
-
</div>
|
|
169
|
-
<div className="w-full h-1.5 dark:bg-white/10 bg-black/10 rounded-full overflow-hidden">
|
|
170
|
-
<div
|
|
171
|
-
className={`h-full rounded-full transition-all ${confidencePercent >= 80 ? 'bg-green-500' : confidencePercent >= 60 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
|
172
|
-
style={{ width: `${confidencePercent}%` }}
|
|
173
|
-
/>
|
|
174
|
-
</div>
|
|
175
|
-
{confidencePercent < 80 && (
|
|
176
|
-
<p className="text-[10px] text-yellow-500 mt-2">
|
|
177
|
-
{t('verify_data_carefully', language)}
|
|
178
|
-
</p>
|
|
179
|
-
)}
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
|
|
183
|
-
{/* Error message */}
|
|
184
|
-
{error && (
|
|
185
|
-
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/30 rounded-xl flex items-center gap-2">
|
|
186
|
-
<AlertCircle className="w-4 h-4 text-red-500 shrink-0" />
|
|
187
|
-
<p className="text-red-400 text-xs font-medium">{error}</p>
|
|
188
|
-
</div>
|
|
189
|
-
)}
|
|
190
|
-
|
|
191
|
-
<div className="space-y-5">
|
|
192
|
-
{/* First Name */}
|
|
193
|
-
<div className="space-y-3">
|
|
194
|
-
<label className="text-xs font-bold uppercase tracking-[0.15em] text-brand ml-1">
|
|
195
|
-
{t('full_name', language)}
|
|
196
|
-
</label>
|
|
197
|
-
<div className="flex items-center px-1 transition-all">
|
|
198
|
-
<User className="text-brand mr-4 w-6 h-6 shrink-0" />
|
|
199
|
-
<div className="flex flex-col w-full">
|
|
200
|
-
<input
|
|
201
|
-
className="bg-transparent border-none p-0 w-full focus:ring-0 text-main font-bold text-lg focus:outline-none placeholder:text-slate-300"
|
|
202
|
-
type="text"
|
|
203
|
-
value={`${formData.firstName} ${formData.lastName}`}
|
|
204
|
-
onChange={(e) => {
|
|
205
|
-
const parts = e.target.value.split(' ');
|
|
206
|
-
handleInputChange('firstName', parts[0] || '');
|
|
207
|
-
handleInputChange('lastName', parts.slice(1).join(' ') || '');
|
|
208
|
-
}}
|
|
209
|
-
placeholder={t('enter_first_name', language)}
|
|
210
|
-
/>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{/* Document Number */}
|
|
216
|
-
<div className="space-y-3">
|
|
217
|
-
<label className="text-xs font-bold uppercase tracking-[0.15em] text-brand ml-1">
|
|
218
|
-
{t('id_number', language)}
|
|
219
|
-
</label>
|
|
220
|
-
<div className="flex items-center px-1 transition-all">
|
|
221
|
-
<CreditCard className="text-brand mr-4 w-6 h-6 shrink-0" />
|
|
222
|
-
<input
|
|
223
|
-
className="bg-transparent border-none p-0 w-full focus:ring-0 text-main font-bold text-lg focus:outline-none placeholder:text-slate-300"
|
|
224
|
-
type="text"
|
|
225
|
-
value={formData.documentNumber}
|
|
226
|
-
onChange={(e) => handleInputChange('documentNumber', e.target.value)}
|
|
227
|
-
placeholder={t('enter_doc_number', language)}
|
|
228
|
-
/>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
{/* Date of Birth */}
|
|
233
|
-
<div className="space-y-3">
|
|
234
|
-
<label className="text-xs font-bold uppercase tracking-[0.15em] text-brand ml-1">
|
|
235
|
-
{t('date_of_birth', language)}
|
|
236
|
-
</label>
|
|
237
|
-
<div className="flex items-center px-1 transition-all">
|
|
238
|
-
<Calendar className="text-brand mr-4 w-6 h-6 shrink-0" />
|
|
239
|
-
<input
|
|
240
|
-
className="bg-transparent border-none p-0 w-full focus:ring-0 text-main font-bold text-lg focus:outline-none placeholder:text-slate-300"
|
|
241
|
-
type="text"
|
|
242
|
-
value={formData.dateOfBirth}
|
|
243
|
-
onChange={(e) => handleInputChange('dateOfBirth', e.target.value)}
|
|
244
|
-
placeholder={t('date_format', language)}
|
|
245
|
-
/>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
|
-
{/* Nationality */}
|
|
250
|
-
<div className="space-y-3">
|
|
251
|
-
<label className="text-xs font-bold uppercase tracking-[0.15em] text-brand ml-1">
|
|
252
|
-
{t('nationality', language)}
|
|
253
|
-
</label>
|
|
254
|
-
<div className="flex items-center px-1 transition-all">
|
|
255
|
-
<Globe className="text-brand mr-4 w-6 h-6 shrink-0" />
|
|
256
|
-
<input
|
|
257
|
-
className="bg-transparent border-none p-0 w-full focus:ring-0 text-main font-bold text-lg focus:outline-none placeholder:text-slate-300"
|
|
258
|
-
type="text"
|
|
259
|
-
value={formData.nationality}
|
|
260
|
-
onChange={(e) => handleInputChange('nationality', e.target.value)}
|
|
261
|
-
placeholder={t('enter_nationality', language)}
|
|
262
|
-
/>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
|
|
266
|
-
{/* Document scan thumbnail */}
|
|
267
|
-
<div className="mt-8 relative h-32 w-full rounded-2xl overflow-hidden border border-brand-20 group">
|
|
268
|
-
<div className="absolute inset-0 bg-gradient-to-tr from-brand-20 to-cyan-500/10 blur-sm" />
|
|
269
|
-
<div className="absolute inset-0 bg-brand/10 flex flex-col items-center justify-center p-4">
|
|
270
|
-
<div className="flex items-center gap-3">
|
|
271
|
-
<CheckCircle2 className="text-brand w-5 h-5" />
|
|
272
|
-
<span className="text-xs font-bold dark:text-slate-200 text-slate-700">
|
|
273
|
-
{t('scanned_successfully', language)}
|
|
274
|
-
</span>
|
|
275
|
-
</div>
|
|
276
|
-
{onRetake && (
|
|
277
|
-
<button
|
|
278
|
-
onClick={onRetake}
|
|
279
|
-
className="mt-2 text-[10px] text-brand underline font-medium"
|
|
280
|
-
type="button"
|
|
281
|
-
>
|
|
282
|
-
{t('retake_photo', language)}
|
|
283
|
-
</button>
|
|
284
|
-
)}
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
<div className="mt-8">
|
|
290
|
-
<button
|
|
291
|
-
onClick={handleConfirm}
|
|
292
|
-
disabled={isSubmitting}
|
|
293
|
-
className="w-full bg-brand hover:bg-brand/90 text-white font-bold py-4 rounded-2xl shadow-lg shadow-brand flex items-center justify-center gap-2 transition-transform active:scale-95 disabled:opacity-70"
|
|
294
|
-
>
|
|
295
|
-
{isSubmitting ? (
|
|
296
|
-
<>
|
|
297
|
-
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
298
|
-
<span>{t('submitting', language)}</span>
|
|
299
|
-
</>
|
|
300
|
-
) : (
|
|
301
|
-
<>
|
|
302
|
-
<span>{t('confirm_mint', language)}</span>
|
|
303
|
-
<Sparkles className="w-5 h-5" />
|
|
304
|
-
</>
|
|
305
|
-
)}
|
|
306
|
-
</button>
|
|
307
|
-
<p className="text-center text-[10px] mt-4 text-slate-500">
|
|
308
|
-
{t('secured_by_casper', language)}
|
|
309
|
-
</p>
|
|
310
|
-
</div>
|
|
311
|
-
</motion.div>
|
|
312
|
-
);
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
export default ReviewData;
|