@casperid/react 1.0.1 → 1.1.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.
Files changed (42) hide show
  1. package/dist/_commonjsHelpers-DKOUU3wS.cjs +2 -0
  2. package/dist/_commonjsHelpers-DKOUU3wS.cjs.map +1 -0
  3. package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
  4. package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
  5. package/dist/camera_utils-BQaOSBPu.js +397 -0
  6. package/dist/camera_utils-BQaOSBPu.js.map +1 -0
  7. package/dist/camera_utils-wchtqrQn.cjs +2 -0
  8. package/dist/camera_utils-wchtqrQn.cjs.map +1 -0
  9. package/dist/face_mesh-DYMPc5Ce.js +4212 -0
  10. package/dist/face_mesh-DYMPc5Ce.js.map +1 -0
  11. package/dist/face_mesh-fEvyDoPt.cjs +19 -0
  12. package/dist/face_mesh-fEvyDoPt.cjs.map +1 -0
  13. package/dist/index.cjs +41 -41
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.mjs +1786 -1501
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/style.css +1 -1
  18. package/package.json +11 -11
  19. package/src/CasperIDModal.tsx +0 -777
  20. package/src/index.css +0 -217
  21. package/src/index.ts +0 -41
  22. package/src/screens/AuthSelection.tsx +0 -221
  23. package/src/screens/DocumentScan.tsx +0 -348
  24. package/src/screens/FaceScan.tsx +0 -368
  25. package/src/screens/IdentityVerified.tsx +0 -51
  26. package/src/screens/L1Setup.tsx +0 -335
  27. package/src/screens/Login.tsx +0 -186
  28. package/src/screens/MintingIdentity.tsx +0 -446
  29. package/src/screens/PasskeyAuth.tsx +0 -259
  30. package/src/screens/PasskeyRegister.tsx +0 -281
  31. package/src/screens/PermissionsRequest.tsx +0 -96
  32. package/src/screens/PinVerification.tsx +0 -321
  33. package/src/screens/ReviewData.tsx +0 -315
  34. package/src/screens/SecurityUpgrade.tsx +0 -83
  35. package/src/screens/VerifyIdentityChoice.tsx +0 -59
  36. package/src/shared/Footer.tsx +0 -13
  37. package/src/shared/GlassContainer.tsx +0 -17
  38. package/src/shared/Header.tsx +0 -62
  39. package/src/types.ts +0 -342
  40. package/src/utils/fetchWithTimeout.ts +0 -52
  41. package/src/utils/i18n.ts +0 -640
  42. 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;