@casperid/react 1.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.
@@ -0,0 +1,446 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Shield, Network, AlertCircle, CheckCircle2, Wallet, Fingerprint } from 'lucide-react';
4
+ import { t } from '../utils/i18n';
5
+
6
+ type VerificationTier = 'layer_1' | 'layer_2' | 'layer_3';
7
+
8
+ interface MintingIdentityProps {
9
+ tier: VerificationTier;
10
+ sessionToken?: string;
11
+ requestId?: string; // Only needed for L2/L3 submission
12
+ onNext: (result?: { wallet?: string; didAddress?: string; credentialHash?: string }) => void;
13
+ onError?: (error: string) => void;
14
+ apiBaseUrl?: string;
15
+ previewMode?: boolean;
16
+ language?: string;
17
+ }
18
+
19
+ type MintingStatus = 'processing' | 'minting' | 'completed' | 'failed';
20
+
21
+ // Tier-specific status messages
22
+ const getStatusMessages = (tier: VerificationTier, language: string) => {
23
+ if (tier === 'layer_1') {
24
+ return {
25
+ processing: {
26
+ title: t('generating_wallet', language),
27
+ subtitle: t('wallet_subtitle', language)
28
+ },
29
+ minting: {
30
+ title: t('minting_did', language),
31
+ subtitle: t('did_subtitle', language)
32
+ },
33
+ completed: {
34
+ title: t('identity_created', language),
35
+ subtitle: t('identity_created_subtitle', language)
36
+ },
37
+ failed: {
38
+ title: t('setup_failed', language),
39
+ subtitle: t('setup_failed_subtitle', language)
40
+ }
41
+ };
42
+ }
43
+
44
+ // L2/L3 messages
45
+ return {
46
+ processing: {
47
+ title: tier === 'layer_2' ? t('processing_liveness', language) : t('processing_documents', language),
48
+ subtitle: t('processing_subtitle', language)
49
+ },
50
+ minting: {
51
+ title: t('minting_identity', language),
52
+ subtitle: t('minting_subtitle', language)
53
+ },
54
+ completed: {
55
+ title: t('verification_submitted', language),
56
+ subtitle: t('verification_submitted_subtitle', language)
57
+ },
58
+ failed: {
59
+ title: t('verification_failed', language),
60
+ subtitle: t('verification_failed_subtitle', language)
61
+ }
62
+ };
63
+ };
64
+
65
+ const MintingIdentity: React.FC<MintingIdentityProps> = ({
66
+ tier,
67
+ sessionToken,
68
+ requestId,
69
+ onNext,
70
+ onError,
71
+ apiBaseUrl = 'https://apis.casperid.com',
72
+ previewMode = false,
73
+ language = 'EN'
74
+ }) => {
75
+ const [status, setStatus] = useState<MintingStatus>('processing');
76
+ const [progress, setProgress] = useState(0);
77
+ const [error, setError] = useState('');
78
+ const [wallet, setWallet] = useState<string | null>(null);
79
+ const [didAddress, setDidAddress] = useState<string | null>(null);
80
+ const [credentialHash, setCredentialHash] = useState<string | null>(null);
81
+ const [humanId, setHumanId] = useState<string | null>(null);
82
+
83
+ // Prevent double execution in React StrictMode
84
+ const hasStartedRef = useRef(false);
85
+
86
+ const statusMessages = getStatusMessages(tier, language);
87
+ const currentMessage = statusMessages[status];
88
+
89
+ // Main setup effect
90
+ useEffect(() => {
91
+ if (hasStartedRef.current) return;
92
+ hasStartedRef.current = true;
93
+
94
+ if (previewMode) {
95
+ runPreviewMode();
96
+ } else {
97
+ runSetup();
98
+ }
99
+ }, []);
100
+
101
+ // Preview mode simulation
102
+ const runPreviewMode = async () => {
103
+ setStatus('processing');
104
+ setProgress(20);
105
+ await sleep(1000);
106
+ setProgress(50);
107
+ await sleep(1000);
108
+ setStatus('minting');
109
+ setProgress(70);
110
+ await sleep(1000);
111
+ setProgress(100);
112
+ setStatus('completed');
113
+
114
+ if (tier === 'layer_1') {
115
+ setWallet('account-hash-abc123...');
116
+ setHumanId('casper-user-1234');
117
+ setDidAddress('did:casper:abc123...');
118
+ } else {
119
+ setCredentialHash('mock-credential-hash-xyz');
120
+ }
121
+
122
+ await sleep(1500);
123
+ onNext({
124
+ wallet: 'account-hash-abc123...',
125
+ didAddress: 'did:casper:abc123...',
126
+ credentialHash: 'mock-credential-hash-xyz'
127
+ });
128
+ };
129
+
130
+ // Real setup based on tier
131
+ const runSetup = async () => {
132
+ try {
133
+ if (tier === 'layer_1') {
134
+ await runL1Setup();
135
+ } else {
136
+ await runL2L3Setup();
137
+ }
138
+ } catch (err) {
139
+ const message = err instanceof Error ? err.message : 'Setup failed';
140
+ setError(message);
141
+ setStatus('failed');
142
+ onError?.(message);
143
+ }
144
+ };
145
+
146
+ // L1 Setup: Wallet + DID creation (synchronous)
147
+ const runL1Setup = async () => {
148
+ // Step 1: Generate wallet
149
+ setStatus('processing');
150
+ setProgress(10);
151
+
152
+ const walletResponse = await fetch(`${apiBaseUrl}/api/passkey/wallet/generate`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Authorization': `Bearer ${sessionToken}`
157
+ },
158
+ credentials: 'include'
159
+ });
160
+
161
+ if (!walletResponse.ok) {
162
+ const errorData = await walletResponse.json().catch(() => ({}));
163
+ throw new Error(errorData.error || 'Failed to generate wallet');
164
+ }
165
+
166
+ const walletData = await walletResponse.json();
167
+ if (!walletData.success || !walletData.walletInfo?.walletAddress) {
168
+ throw new Error(walletData.error || 'Wallet generation failed');
169
+ }
170
+
171
+ setWallet(walletData.walletInfo.walletAddress);
172
+ if (walletData.walletInfo.humanId) {
173
+ setHumanId(walletData.walletInfo.humanId);
174
+ }
175
+ setProgress(50);
176
+
177
+ // Step 2: Mint DID
178
+ setStatus('minting');
179
+ setProgress(60);
180
+
181
+ const didResponse = await fetch(`${apiBaseUrl}/api/passkey/did/mint`, {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ 'Authorization': `Bearer ${sessionToken}`
186
+ },
187
+ credentials: 'include'
188
+ });
189
+
190
+ if (!didResponse.ok) {
191
+ const errorData = await didResponse.json().catch(() => ({}));
192
+ throw new Error(errorData.error || 'Failed to mint DID');
193
+ }
194
+
195
+ const didData = await didResponse.json();
196
+ if (!didData.success) {
197
+ throw new Error(didData.error || 'DID minting failed');
198
+ }
199
+
200
+ setDidAddress(didData.did_address);
201
+ setCredentialHash(didData.transaction_hash);
202
+ setProgress(100);
203
+ setStatus('completed');
204
+
205
+ // Auto-proceed after showing success
206
+ await sleep(1500);
207
+ onNext({
208
+ wallet: walletData.walletInfo.walletAddress,
209
+ didAddress: didData.did_address,
210
+ credentialHash: didData.transaction_hash
211
+ });
212
+ };
213
+
214
+ // L2/L3 Setup: Submit verification (no polling)
215
+ const runL2L3Setup = async () => {
216
+ setStatus('processing');
217
+ setProgress(30);
218
+
219
+ // For L2/L3, we just confirm the submission was successful
220
+ // The actual verification happens in background, user tier upgrades when ready
221
+ await sleep(500);
222
+ setProgress(60);
223
+
224
+ setStatus('minting');
225
+ setProgress(80);
226
+
227
+ await sleep(500);
228
+ setProgress(100);
229
+ setStatus('completed');
230
+
231
+ // Auto-proceed after showing success
232
+ await sleep(1500);
233
+ onNext({ credentialHash: requestId });
234
+ };
235
+
236
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
237
+
238
+ const handleRetry = () => {
239
+ setStatus('processing');
240
+ setProgress(0);
241
+ setError('');
242
+ hasStartedRef.current = false;
243
+
244
+ if (previewMode) {
245
+ runPreviewMode();
246
+ } else {
247
+ runSetup();
248
+ }
249
+ };
250
+
251
+ // Get tier-specific icon
252
+ const getTierIcon = () => {
253
+ if (tier === 'layer_1') {
254
+ return status === 'minting' ? <Fingerprint className="text-brand w-4 h-4" /> : <Wallet className="text-brand w-4 h-4" />;
255
+ }
256
+ return <Shield className="text-brand w-4 h-4" />;
257
+ };
258
+
259
+ // Get status label for transaction status
260
+ const getStatusLabel = () => {
261
+ if (tier === 'layer_1') {
262
+ if (status === 'processing') return t('creating_wallet', language);
263
+ if (status === 'minting') return t('minting_did_chain', language);
264
+ if (status === 'completed') return t('identity_live', language);
265
+ } else {
266
+ if (status === 'processing') return t('validating_docs', language);
267
+ if (status === 'minting') return t('submitting_verification', language);
268
+ if (status === 'completed') return t('verification_queued', language);
269
+ }
270
+ return '';
271
+ };
272
+
273
+ return (
274
+ <motion.div
275
+ initial={{ opacity: 0 }}
276
+ animate={{ opacity: 1 }}
277
+ exit={{ opacity: 0 }}
278
+ className="flex-1 flex flex-col items-center justify-center px-8 text-center"
279
+ >
280
+ {/* Rotating card animation */}
281
+ <div className="relative w-64 h-64 flex items-center justify-center mb-12">
282
+ <div className="absolute inset-0 rounded-full border-4 border-dashed border-brand-20 animate-[spin_10s_linear_infinite]" />
283
+ <div className="absolute inset-4 rounded-full border-2 border-[#6DE8EC]/20 animate-[spin_6s_linear_infinite_reverse]" />
284
+
285
+ <motion.div
286
+ animate={status !== 'failed' ? { rotateY: [0, 180, 360], y: [-10, 10, -10] } : {}}
287
+ transition={{ duration: 6, repeat: Infinity, ease: 'easeInOut' }}
288
+ className={`relative w-40 h-56 backdrop-blur-xl border rounded-2xl flex flex-col p-4 ${status === 'completed'
289
+ ? 'bg-green-500/10 border-green-500/30 shadow-[0_0_50px_-12px_rgba(34,197,94,0.5)]'
290
+ : status === 'failed'
291
+ ? 'bg-red-500/10 border-red-500/30 shadow-[0_0_50px_-12px_rgba(239,68,68,0.5)]'
292
+ : 'bg-brand/10 border-brand-30 shadow-[0_0_50px_-12px_rgba(114,43,238,0.5)]'
293
+ }`}
294
+ >
295
+ <div className="flex justify-between items-start mb-6">
296
+ <div className={`w-8 h-8 rounded flex items-center justify-center ${status === 'completed'
297
+ ? 'bg-green-500/30'
298
+ : status === 'failed'
299
+ ? 'bg-red-500/30'
300
+ : 'bg-brand/30'
301
+ }`}>
302
+ {status === 'completed' ? (
303
+ <CheckCircle2 className="text-green-500 w-4 h-4" />
304
+ ) : status === 'failed' ? (
305
+ <AlertCircle className="text-red-500 w-4 h-4" />
306
+ ) : (
307
+ getTierIcon()
308
+ )}
309
+ </div>
310
+ <div className={`w-6 h-1 rounded-full ${status === 'completed'
311
+ ? 'bg-green-500/40'
312
+ : status === 'failed'
313
+ ? 'bg-red-500/40'
314
+ : 'bg-brand/40'
315
+ }`} />
316
+ </div>
317
+ <div className="space-y-3 mb-auto">
318
+ <div className={`h-2 w-3/4 rounded-full ${status === 'completed'
319
+ ? 'bg-green-500/20'
320
+ : status === 'failed'
321
+ ? 'bg-red-500/20'
322
+ : 'bg-brand/20'
323
+ }`} />
324
+ <div className={`h-2 w-1/2 rounded-full ${status === 'completed'
325
+ ? 'bg-green-500/20'
326
+ : status === 'failed'
327
+ ? 'bg-red-500/20'
328
+ : 'bg-brand/20'
329
+ }`} />
330
+ </div>
331
+ <div className="flex items-center gap-2 mt-4">
332
+ <div className={`size-6 rounded-full overflow-hidden border border-white/10 ${status === 'completed'
333
+ ? 'bg-gradient-to-tr from-green-500 to-[#6DE8EC]'
334
+ : status === 'failed'
335
+ ? 'bg-gradient-to-tr from-red-500 to-orange-500'
336
+ : 'bg-gradient-to-tr from-brand to-[#6DE8EC]'
337
+ }`} />
338
+ <div className="flex flex-col items-start max-w-[80px]">
339
+ {humanId ? (
340
+ <motion.span
341
+ initial={{ opacity: 0 }}
342
+ animate={{ opacity: 1 }}
343
+ className="text-[10px] font-bold text-white/90 truncate w-full text-left"
344
+ >
345
+ {humanId}
346
+ </motion.span>
347
+ ) : (
348
+ <div className="h-1.5 w-12 bg-white/20 rounded-full mb-1" />
349
+ )}
350
+ <div className="h-1 w-8 bg-white/10 rounded-full" />
351
+ </div>
352
+ </div>
353
+ {status !== 'failed' && status !== 'completed' && (
354
+ <motion.div
355
+ animate={{ top: ['0%', '100%', '0%'] }}
356
+ transition={{ duration: 2, repeat: Infinity }}
357
+ className="absolute inset-0 bg-gradient-to-b from-transparent via-brand-10 to-transparent h-1/2 w-full"
358
+ />
359
+ )}
360
+ </motion.div>
361
+ </div>
362
+
363
+ <div className="space-y-4">
364
+ <h2 className={`text-2xl font-bold bg-clip-text ${status === 'completed'
365
+ ? 'text-green-500'
366
+ : status === 'failed'
367
+ ? 'text-red-500'
368
+ : 'dark:bg-gradient-to-r dark:from-slate-100 dark:to-slate-400 bg-slate-900 dark:text-transparent text-slate-900'
369
+ }`}>
370
+ {currentMessage.title}
371
+ </h2>
372
+ <p className="dark:text-slate-400 text-slate-500 text-sm leading-relaxed max-w-[280px] mx-auto">
373
+ {currentMessage.subtitle}
374
+ </p>
375
+ </div>
376
+
377
+ {/* Error with retry button */}
378
+ {status === 'failed' && (
379
+ <div className="mt-6 space-y-3">
380
+ <p className="text-red-500 text-sm">{error}</p>
381
+ <button
382
+ onClick={handleRetry}
383
+ className="px-6 py-3 bg-brand text-white rounded-2xl font-bold"
384
+ >
385
+ {t('try_again', language)}
386
+ </button>
387
+ </div>
388
+ )}
389
+
390
+ {/* Wallet/DID display for L1 */}
391
+ {tier === 'layer_1' && wallet && status === 'completed' && (
392
+ <div className="mt-6 p-3 dark:bg-white/5 bg-black/5 rounded-xl border dark:border-white/10 border-black/5">
393
+ <p className="text-[10px] text-muted mb-1">{t('your_wallet', language)}</p>
394
+ <p className="text-xs font-mono text-main break-all">
395
+ {wallet.slice(0, 20)}...{wallet.slice(-10)}
396
+ </p>
397
+ </div>
398
+ )}
399
+
400
+ {/* Credential hash display for L2/L3 */}
401
+ {tier !== 'layer_1' && credentialHash && status === 'completed' && (
402
+ <div className="mt-6 p-3 dark:bg-white/5 bg-black/5 rounded-xl border dark:border-white/10 border-black/5">
403
+ <p className="text-[10px] text-muted mb-1">{t('verification_id', language)}</p>
404
+ <p className="text-xs font-mono text-main break-all">
405
+ {credentialHash.slice(0, 20)}...
406
+ </p>
407
+ </div>
408
+ )}
409
+
410
+ {/* Transaction status (only show when not failed) */}
411
+ {status !== 'failed' && (
412
+ <div className="w-full mt-12 p-6 dark:bg-brand/5 bg-brand/5 border-t dark:border-white/5 border-black/5">
413
+ <div className="flex justify-between items-center mb-3">
414
+ <span className="text-xs font-medium text-brand uppercase tracking-widest">
415
+ {tier === 'layer_1' ? t('setup_status', language) : t('transaction_status', language)}
416
+ </span>
417
+ <span className={`text-xs font-bold ${status === 'completed' ? 'text-green-500' : 'text-[#6DE8EC]'
418
+ }`}>
419
+ {t('complete_percent', language).replace('{percent}', progress.toString())}
420
+ </span>
421
+ </div>
422
+ <div className="w-full h-1.5 dark:bg-brand/10 bg-brand/10 rounded-full overflow-hidden mb-4">
423
+ <motion.div
424
+ initial={{ width: '0%' }}
425
+ animate={{ width: `${progress}%` }}
426
+ transition={{ duration: 0.5 }}
427
+ className={`h-full rounded-full ${status === 'completed'
428
+ ? 'bg-gradient-to-r from-green-500 to-[#6DE8EC]'
429
+ : 'bg-gradient-to-r from-brand to-[#6DE8EC]'
430
+ } shadow-[0_0_10px_rgba(114,43,238,0.5)]`}
431
+ />
432
+ </div>
433
+ <div className="flex items-center gap-3 px-3 py-2 dark:bg-brand/10 bg-brand/10 rounded-xl border dark:border-brand-10 border-brand-5">
434
+ <Network className={`w-4 h-4 ${status === 'completed' ? 'text-green-500' : 'text-brand'
435
+ }`} />
436
+ <p className="text-[10px] dark:text-slate-300 text-slate-600 font-medium">
437
+ {getStatusLabel()}
438
+ </p>
439
+ </div>
440
+ </div>
441
+ )}
442
+ </motion.div>
443
+ );
444
+ };
445
+
446
+ export default MintingIdentity;