@casperid/react 1.0.0 → 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.
- package/README.md +1 -1
- package/dist/_commonjsHelpers-DKOUU3wS.cjs +2 -0
- package/dist/_commonjsHelpers-DKOUU3wS.cjs.map +1 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
- package/dist/camera_utils-BQaOSBPu.js +397 -0
- package/dist/camera_utils-BQaOSBPu.js.map +1 -0
- package/dist/camera_utils-wchtqrQn.cjs +2 -0
- package/dist/camera_utils-wchtqrQn.cjs.map +1 -0
- package/dist/face_mesh-DYMPc5Ce.js +4212 -0
- package/dist/face_mesh-DYMPc5Ce.js.map +1 -0
- package/dist/face_mesh-fEvyDoPt.cjs +19 -0
- package/dist/face_mesh-fEvyDoPt.cjs.map +1 -0
- package/dist/index.cjs +41 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1786 -1501
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +23 -10
- 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
package/src/screens/L1Setup.tsx
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { Wallet, Fingerprint, CheckCircle2, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
|
|
4
|
-
import { t } from '../utils/i18n';
|
|
5
|
-
import type { WalletGenerateResponse, DIDMintResponse } from '../types';
|
|
6
|
-
|
|
7
|
-
interface L1SetupProps {
|
|
8
|
-
sessionToken: string;
|
|
9
|
-
onSuccess: (wallet: string, didAddress: string) => void;
|
|
10
|
-
onError?: (error: string) => void;
|
|
11
|
-
apiBaseUrl?: string;
|
|
12
|
-
previewMode?: boolean;
|
|
13
|
-
language?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
type SetupStep = 'wallet' | 'did' | 'complete' | 'error';
|
|
17
|
-
|
|
18
|
-
const L1Setup: React.FC<L1SetupProps> = ({
|
|
19
|
-
sessionToken,
|
|
20
|
-
onSuccess,
|
|
21
|
-
onError,
|
|
22
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
23
|
-
previewMode = false,
|
|
24
|
-
language = 'EN'
|
|
25
|
-
}) => {
|
|
26
|
-
const [currentStep, setCurrentStep] = useState<SetupStep>('wallet');
|
|
27
|
-
const [progress, setProgress] = useState(0);
|
|
28
|
-
const [error, setError] = useState('');
|
|
29
|
-
const [wallet, setWallet] = useState<string | null>(null);
|
|
30
|
-
const [didAddress, setDidAddress] = useState<string | null>(null);
|
|
31
|
-
|
|
32
|
-
// Prevent double execution in React StrictMode
|
|
33
|
-
const hasStartedRef = useRef(false);
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (hasStartedRef.current) return;
|
|
37
|
-
hasStartedRef.current = true;
|
|
38
|
-
runSetup();
|
|
39
|
-
}, []);
|
|
40
|
-
|
|
41
|
-
const runSetup = async () => {
|
|
42
|
-
setError('');
|
|
43
|
-
setCurrentStep('wallet');
|
|
44
|
-
setProgress(0);
|
|
45
|
-
|
|
46
|
-
if (previewMode) {
|
|
47
|
-
// Mock the setup process
|
|
48
|
-
await mockSetup();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// Step 1: Generate wallet (0-50%)
|
|
54
|
-
setProgress(10);
|
|
55
|
-
const walletResult = await generateWallet();
|
|
56
|
-
setWallet(walletResult);
|
|
57
|
-
setProgress(50);
|
|
58
|
-
|
|
59
|
-
// Step 2: Mint DID (50-100%)
|
|
60
|
-
setCurrentStep('did');
|
|
61
|
-
setProgress(60);
|
|
62
|
-
const didResult = await mintDID();
|
|
63
|
-
setDidAddress(didResult);
|
|
64
|
-
setProgress(100);
|
|
65
|
-
|
|
66
|
-
// Complete
|
|
67
|
-
setCurrentStep('complete');
|
|
68
|
-
setTimeout(() => {
|
|
69
|
-
onSuccess(walletResult, didResult);
|
|
70
|
-
}, 1500);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
const message = err instanceof Error ? err.message : 'Setup failed';
|
|
73
|
-
setError(message);
|
|
74
|
-
setCurrentStep('error');
|
|
75
|
-
onError?.(message);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const mockSetup = async () => {
|
|
80
|
-
// Wallet generation
|
|
81
|
-
setProgress(10);
|
|
82
|
-
await sleep(800);
|
|
83
|
-
setProgress(30);
|
|
84
|
-
await sleep(500);
|
|
85
|
-
setProgress(50);
|
|
86
|
-
setWallet('0x1234...5678');
|
|
87
|
-
|
|
88
|
-
// DID minting
|
|
89
|
-
setCurrentStep('did');
|
|
90
|
-
setProgress(60);
|
|
91
|
-
await sleep(800);
|
|
92
|
-
setProgress(80);
|
|
93
|
-
await sleep(500);
|
|
94
|
-
setProgress(100);
|
|
95
|
-
setDidAddress('did:casper:abc123');
|
|
96
|
-
|
|
97
|
-
// Complete
|
|
98
|
-
setCurrentStep('complete');
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
onSuccess('0x1234...5678', 'did:casper:abc123');
|
|
101
|
-
}, 1500);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
105
|
-
|
|
106
|
-
const generateWallet = async (): Promise<string> => {
|
|
107
|
-
const response = await fetch(`${apiBaseUrl}/api/passkey/wallet/generate`, {
|
|
108
|
-
method: 'POST',
|
|
109
|
-
headers: {
|
|
110
|
-
'Content-Type': 'application/json',
|
|
111
|
-
'Authorization': `Bearer ${sessionToken}`
|
|
112
|
-
},
|
|
113
|
-
credentials: 'include'
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
const errorData = await response.json().catch(() => ({}));
|
|
118
|
-
throw new Error(errorData.error || 'Failed to generate wallet');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const data: WalletGenerateResponse = await response.json();
|
|
122
|
-
if (!data.success || !data.walletInfo?.walletAddress) {
|
|
123
|
-
throw new Error(data.error || 'Wallet generation failed');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return data.walletInfo.walletAddress;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const mintDID = async (): Promise<string> => {
|
|
130
|
-
const response = await fetch(`${apiBaseUrl}/api/passkey/did/mint`, {
|
|
131
|
-
method: 'POST',
|
|
132
|
-
headers: {
|
|
133
|
-
'Content-Type': 'application/json',
|
|
134
|
-
'Authorization': `Bearer ${sessionToken}`
|
|
135
|
-
},
|
|
136
|
-
credentials: 'include'
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (!response.ok) {
|
|
140
|
-
const errorData = await response.json().catch(() => ({}));
|
|
141
|
-
throw new Error(errorData.error || 'Failed to mint DID');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const data: DIDMintResponse = await response.json();
|
|
145
|
-
if (!data.success || !data.did_address) {
|
|
146
|
-
throw new Error(data.error || 'DID minting failed');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return data.did_address;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const getStepIcon = (step: SetupStep) => {
|
|
153
|
-
switch (step) {
|
|
154
|
-
case 'wallet':
|
|
155
|
-
return <Wallet className="w-8 h-8" />;
|
|
156
|
-
case 'did':
|
|
157
|
-
return <Fingerprint className="w-8 h-8" />;
|
|
158
|
-
case 'complete':
|
|
159
|
-
return <CheckCircle2 className="w-8 h-8" />;
|
|
160
|
-
case 'error':
|
|
161
|
-
return <AlertCircle className="w-8 h-8" />;
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const getStepText = () => {
|
|
166
|
-
switch (currentStep) {
|
|
167
|
-
case 'wallet':
|
|
168
|
-
return t('generating_wallet', language);
|
|
169
|
-
case 'did':
|
|
170
|
-
return t('minting_did', language);
|
|
171
|
-
case 'complete':
|
|
172
|
-
return t('l1_complete', language);
|
|
173
|
-
case 'error':
|
|
174
|
-
return t('l1_failed', language);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const getStepColor = () => {
|
|
179
|
-
switch (currentStep) {
|
|
180
|
-
case 'complete':
|
|
181
|
-
return 'text-green-400';
|
|
182
|
-
case 'error':
|
|
183
|
-
return 'text-red-400';
|
|
184
|
-
default:
|
|
185
|
-
return 'text-brand';
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const getBgColor = () => {
|
|
190
|
-
switch (currentStep) {
|
|
191
|
-
case 'complete':
|
|
192
|
-
return 'bg-green-500/20 border-green-400';
|
|
193
|
-
case 'error':
|
|
194
|
-
return 'bg-red-500/10 border-red-400';
|
|
195
|
-
default:
|
|
196
|
-
return 'bg-brand/20 border-brand';
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
return (
|
|
201
|
-
<motion.div
|
|
202
|
-
initial={{ opacity: 0 }}
|
|
203
|
-
animate={{ opacity: 1 }}
|
|
204
|
-
exit={{ opacity: 0 }}
|
|
205
|
-
className="flex-1 flex flex-col"
|
|
206
|
-
>
|
|
207
|
-
{/* Header */}
|
|
208
|
-
<div className="p-6 pb-2">
|
|
209
|
-
<div className="flex justify-between items-center mb-4">
|
|
210
|
-
<span className="text-xs font-semibold uppercase tracking-widest text-brand/80">
|
|
211
|
-
{t('verification_step', language)}
|
|
212
|
-
</span>
|
|
213
|
-
<span className="text-xs font-bold text-muted">L1 {t('secure_sdk', language)}</span>
|
|
214
|
-
</div>
|
|
215
|
-
<div className="w-full bg-brand/10 h-1.5 rounded-full overflow-hidden">
|
|
216
|
-
<motion.div
|
|
217
|
-
className="bg-brand h-full rounded-full"
|
|
218
|
-
initial={{ width: '0%' }}
|
|
219
|
-
animate={{ width: `${progress}%` }}
|
|
220
|
-
transition={{ duration: 0.5, ease: 'easeOut' }}
|
|
221
|
-
/>
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
{/* Main content */}
|
|
226
|
-
<div className="flex-1 flex flex-col items-center justify-center px-8 text-center gap-8">
|
|
227
|
-
{/* Animated icon */}
|
|
228
|
-
<motion.div
|
|
229
|
-
animate={currentStep !== 'complete' && currentStep !== 'error' ? {
|
|
230
|
-
scale: [1, 1.05, 1],
|
|
231
|
-
opacity: [1, 0.8, 1]
|
|
232
|
-
} : {}}
|
|
233
|
-
transition={{
|
|
234
|
-
duration: 2,
|
|
235
|
-
repeat: currentStep !== 'complete' && currentStep !== 'error' ? Infinity : 0,
|
|
236
|
-
ease: "easeInOut"
|
|
237
|
-
}}
|
|
238
|
-
className={`w-24 h-24 rounded-full border-2 flex items-center justify-center ${getBgColor()}`}
|
|
239
|
-
>
|
|
240
|
-
<span className={getStepColor()}>
|
|
241
|
-
{currentStep !== 'complete' && currentStep !== 'error' ? (
|
|
242
|
-
<Loader2 className="w-10 h-10 animate-spin" />
|
|
243
|
-
) : (
|
|
244
|
-
getStepIcon(currentStep)
|
|
245
|
-
)}
|
|
246
|
-
</span>
|
|
247
|
-
</motion.div>
|
|
248
|
-
|
|
249
|
-
{/* Status text */}
|
|
250
|
-
<div className="space-y-4">
|
|
251
|
-
<h1 className="text-2xl font-bold tracking-tight text-main leading-tight">
|
|
252
|
-
{t('l1_setup_title', language)}
|
|
253
|
-
</h1>
|
|
254
|
-
<p className="text-muted text-base leading-relaxed font-medium">
|
|
255
|
-
{getStepText()}
|
|
256
|
-
</p>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
{/* Progress details */}
|
|
260
|
-
<div className="w-full max-w-xs space-y-3">
|
|
261
|
-
<div className={`flex items-center gap-3 p-3 rounded-xl ${
|
|
262
|
-
currentStep === 'wallet' ? 'bg-brand/10' :
|
|
263
|
-
progress >= 50 ? 'bg-green-500/10' : 'bg-slate-100/50 dark:bg-white/5'
|
|
264
|
-
}`}>
|
|
265
|
-
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
|
266
|
-
progress >= 50 ? 'bg-green-500/20' :
|
|
267
|
-
currentStep === 'wallet' ? 'bg-brand/20' : 'bg-slate-200 dark:bg-white/10'
|
|
268
|
-
}`}>
|
|
269
|
-
{progress >= 50 ? (
|
|
270
|
-
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
|
271
|
-
) : currentStep === 'wallet' ? (
|
|
272
|
-
<Loader2 className="w-4 h-4 text-brand animate-spin" />
|
|
273
|
-
) : (
|
|
274
|
-
<Wallet className="w-4 h-4 text-muted" />
|
|
275
|
-
)}
|
|
276
|
-
</div>
|
|
277
|
-
<div className="flex-1 text-left">
|
|
278
|
-
<p className={`text-sm font-medium ${progress >= 50 ? 'text-green-400' : 'text-main'}`}>
|
|
279
|
-
{t('generating_wallet', language)}
|
|
280
|
-
</p>
|
|
281
|
-
{wallet && <p className="text-xs text-muted truncate">{wallet}</p>}
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
<div className={`flex items-center gap-3 p-3 rounded-xl ${
|
|
286
|
-
currentStep === 'did' ? 'bg-brand/10' :
|
|
287
|
-
progress >= 100 ? 'bg-green-500/10' : 'bg-slate-100/50 dark:bg-white/5'
|
|
288
|
-
}`}>
|
|
289
|
-
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
|
290
|
-
progress >= 100 ? 'bg-green-500/20' :
|
|
291
|
-
currentStep === 'did' ? 'bg-brand/20' : 'bg-slate-200 dark:bg-white/10'
|
|
292
|
-
}`}>
|
|
293
|
-
{progress >= 100 ? (
|
|
294
|
-
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
|
295
|
-
) : currentStep === 'did' ? (
|
|
296
|
-
<Loader2 className="w-4 h-4 text-brand animate-spin" />
|
|
297
|
-
) : (
|
|
298
|
-
<Fingerprint className="w-4 h-4 text-muted" />
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
301
|
-
<div className="flex-1 text-left">
|
|
302
|
-
<p className={`text-sm font-medium ${progress >= 100 ? 'text-green-400' : 'text-main'}`}>
|
|
303
|
-
{t('minting_did', language)}
|
|
304
|
-
</p>
|
|
305
|
-
{didAddress && <p className="text-xs text-muted truncate">{didAddress}</p>}
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
{/* Error state retry button */}
|
|
311
|
-
{currentStep === 'error' && (
|
|
312
|
-
<div className="space-y-2">
|
|
313
|
-
<p className="text-red-500 text-sm">{error}</p>
|
|
314
|
-
<button
|
|
315
|
-
onClick={runSetup}
|
|
316
|
-
className="flex items-center gap-2 px-6 py-3 bg-brand hover:bg-brand/90 text-white font-bold rounded-xl transition-colors"
|
|
317
|
-
>
|
|
318
|
-
<RefreshCw className="w-4 h-4" />
|
|
319
|
-
{t('try_again', language)}
|
|
320
|
-
</button>
|
|
321
|
-
</div>
|
|
322
|
-
)}
|
|
323
|
-
</div>
|
|
324
|
-
|
|
325
|
-
{/* Footer */}
|
|
326
|
-
<div className="p-8">
|
|
327
|
-
<p className="text-center text-xs text-muted">
|
|
328
|
-
{t('l1_setup_subtitle', language)}
|
|
329
|
-
</p>
|
|
330
|
-
</div>
|
|
331
|
-
</motion.div>
|
|
332
|
-
);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
export default L1Setup;
|
package/src/screens/Login.tsx
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { Mail, Verified, Lock, ArrowRight, Loader2 } from 'lucide-react';
|
|
4
|
-
import type { SendOTPResponse } from '../types';
|
|
5
|
-
import { t } from '../utils/i18n';
|
|
6
|
-
|
|
7
|
-
interface LoginProps {
|
|
8
|
-
onNext: (email: string, verificationId: string) => void;
|
|
9
|
-
onPasskeyUser?: (email: string) => void;
|
|
10
|
-
onError?: (error: string) => void;
|
|
11
|
-
apiBaseUrl?: string;
|
|
12
|
-
previewMode?: boolean;
|
|
13
|
-
language?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const Login: React.FC<LoginProps> = ({
|
|
17
|
-
onNext,
|
|
18
|
-
onPasskeyUser,
|
|
19
|
-
onError,
|
|
20
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
21
|
-
previewMode = false,
|
|
22
|
-
language = 'EN'
|
|
23
|
-
}) => {
|
|
24
|
-
const [email, setEmail] = useState('');
|
|
25
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
26
|
-
const [error, setError] = useState('');
|
|
27
|
-
|
|
28
|
-
const handleSubmit = async () => {
|
|
29
|
-
if (!email.trim()) {
|
|
30
|
-
setError(t('error_email_required', language));
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Basic email validation
|
|
35
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
36
|
-
if (!emailRegex.test(email)) {
|
|
37
|
-
setError(t('error_invalid_email', language));
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
setIsLoading(true);
|
|
42
|
-
setError('');
|
|
43
|
-
|
|
44
|
-
if (previewMode) {
|
|
45
|
-
// Mock delay and success for preview
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
setIsLoading(false);
|
|
48
|
-
onNext(email.trim().toLowerCase(), 'mock-v-id-123456789');
|
|
49
|
-
}, 1000);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
const response = await fetch(`${apiBaseUrl}/api/auth/send-otp`, {
|
|
55
|
-
method: 'POST',
|
|
56
|
-
headers: {
|
|
57
|
-
'Content-Type': 'application/json',
|
|
58
|
-
},
|
|
59
|
-
body: JSON.stringify({ email: email.trim().toLowerCase() }),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
const errorData = await response.json().catch(() => ({}));
|
|
64
|
-
throw new Error(errorData.error || `Request failed with status ${response.status}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const data: SendOTPResponse = await response.json();
|
|
68
|
-
|
|
69
|
-
if (data.success) {
|
|
70
|
-
// Returning user with passkey - route to passkey auth
|
|
71
|
-
if (data.userExists && data.hasPasskey) {
|
|
72
|
-
if (onPasskeyUser) {
|
|
73
|
-
onPasskeyUser(email.trim().toLowerCase());
|
|
74
|
-
} else {
|
|
75
|
-
// Fallback: show message if no handler provided
|
|
76
|
-
setError(t('error_passkey_registered', language));
|
|
77
|
-
}
|
|
78
|
-
setIsLoading(false);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// New user or user without passkey - proceed to OTP
|
|
83
|
-
if (data.verificationId) {
|
|
84
|
-
onNext(email.trim().toLowerCase(), data.verificationId);
|
|
85
|
-
} else {
|
|
86
|
-
setError(t('error_send_code_failed', language));
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
setError(data.error || t('error_send_code_failed', language));
|
|
90
|
-
onError?.(data.error || t('error_send_code_failed', language));
|
|
91
|
-
}
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const errorMessage = t('error_connection_failed', language);
|
|
94
|
-
setError(errorMessage);
|
|
95
|
-
onError?.(errorMessage);
|
|
96
|
-
} finally {
|
|
97
|
-
setIsLoading(false);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
102
|
-
if (e.key === 'Enter' && !isLoading) {
|
|
103
|
-
handleSubmit();
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<motion.div
|
|
109
|
-
initial={{ opacity: 0, x: 20 }}
|
|
110
|
-
animate={{ opacity: 1, x: 0 }}
|
|
111
|
-
exit={{ opacity: 0, x: -20 }}
|
|
112
|
-
className="flex-1 flex flex-col px-8 pt-12"
|
|
113
|
-
>
|
|
114
|
-
<div className="mb-8">
|
|
115
|
-
<h2 className="text-3xl font-extrabold text-main tracking-tight leading-tight">{t('welcome_back', language)}</h2>
|
|
116
|
-
<p className="text-muted mt-3 text-sm leading-relaxed">
|
|
117
|
-
{t('enter_email', language)}
|
|
118
|
-
</p>
|
|
119
|
-
</div>
|
|
120
|
-
|
|
121
|
-
<div className="space-y-6">
|
|
122
|
-
<div className="space-y-2">
|
|
123
|
-
<label className="text-xs font-bold text-slate-500 uppercase tracking-widest ml-1">
|
|
124
|
-
{t('email_address', language)}
|
|
125
|
-
</label>
|
|
126
|
-
<div className="relative">
|
|
127
|
-
<input
|
|
128
|
-
className={`w-full dark:bg-black/20 bg-slate-100/50 border ${error ? 'border-red-400 dark:border-red-500' : 'dark:border-white/10 border-slate-200'} rounded-2xl h-14 px-5 text-main placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-30 transition-all font-medium`}
|
|
129
|
-
placeholder="name@example.com"
|
|
130
|
-
type="email"
|
|
131
|
-
value={email}
|
|
132
|
-
onChange={(e) => {
|
|
133
|
-
setEmail(e.target.value);
|
|
134
|
-
if (error) setError('');
|
|
135
|
-
}}
|
|
136
|
-
onKeyDown={handleKeyDown}
|
|
137
|
-
disabled={isLoading}
|
|
138
|
-
autoComplete="email"
|
|
139
|
-
autoFocus
|
|
140
|
-
/>
|
|
141
|
-
<div className="absolute right-4 top-1/2 -translate-y-1/2 flex items-center text-slate-400">
|
|
142
|
-
<Mail className="w-5 h-5" />
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
{error && (
|
|
146
|
-
<p className="text-red-500 text-xs font-medium ml-1 mt-1">{error}</p>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
<button
|
|
150
|
-
onClick={handleSubmit}
|
|
151
|
-
disabled={isLoading}
|
|
152
|
-
className="w-full h-14 bg-brand hover:bg-brand/90 text-white font-bold rounded-2xl shadow-xl shadow-brand flex items-center justify-center gap-2 transition-transform active:scale-[0.98] disabled:opacity-70 disabled:cursor-not-allowed"
|
|
153
|
-
>
|
|
154
|
-
{isLoading ? (
|
|
155
|
-
<>
|
|
156
|
-
<Loader2 className="w-5 h-5 animate-spin" />
|
|
157
|
-
<span>{t('sending_code', language)}</span>
|
|
158
|
-
</>
|
|
159
|
-
) : (
|
|
160
|
-
<>
|
|
161
|
-
<span>{t('continue', language)}</span>
|
|
162
|
-
<ArrowRight className="w-5 h-5" />
|
|
163
|
-
</>
|
|
164
|
-
)}
|
|
165
|
-
</button>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div className="mt-auto pb-8">
|
|
169
|
-
<div className="p-4 dark:bg-white/5 bg-slate-100/50 rounded-2xl mb-6">
|
|
170
|
-
<div className="flex items-start gap-3">
|
|
171
|
-
<Verified className="text-[#6DE8EC] w-5 h-5 mt-0.5 shrink-0" />
|
|
172
|
-
<p className="text-[11px] text-muted leading-normal font-medium">
|
|
173
|
-
{t('security_notice', language)}
|
|
174
|
-
</p>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
<div className="flex items-center justify-center gap-2 opacity-50">
|
|
178
|
-
<Lock className="text-muted w-3 h-3" />
|
|
179
|
-
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted">{t('e2e_encrypted', language)}</span>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
</motion.div>
|
|
183
|
-
);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
export default Login;
|