@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.
- 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 +11 -11
- 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,259 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { Fingerprint, Loader2, ShieldCheck, AlertCircle, RefreshCw } from 'lucide-react';
|
|
4
|
-
import { t } from '../utils/i18n';
|
|
5
|
-
import {
|
|
6
|
-
isWebAuthnSupported,
|
|
7
|
-
isPlatformAuthenticatorAvailable,
|
|
8
|
-
authenticateWithPasskey
|
|
9
|
-
} from '../utils/webauthn';
|
|
10
|
-
import type { PasskeyAuthInitResponse, PasskeyAuthCompleteResponse } from '../types';
|
|
11
|
-
|
|
12
|
-
interface PasskeyAuthProps {
|
|
13
|
-
email: string;
|
|
14
|
-
onSuccess: (sessionToken: string, user: PasskeyAuthCompleteResponse['user'], businessToken?: string) => void;
|
|
15
|
-
onError?: (error: string) => void;
|
|
16
|
-
onFallbackToOTP?: () => void;
|
|
17
|
-
apiBaseUrl?: string;
|
|
18
|
-
previewMode?: boolean;
|
|
19
|
-
language?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const PasskeyAuth: React.FC<PasskeyAuthProps> = ({
|
|
23
|
-
email,
|
|
24
|
-
onSuccess,
|
|
25
|
-
onError,
|
|
26
|
-
onFallbackToOTP,
|
|
27
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
28
|
-
previewMode = false,
|
|
29
|
-
language = 'EN'
|
|
30
|
-
}) => {
|
|
31
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
-
const [error, setError] = useState('');
|
|
33
|
-
const [isSupported, setIsSupported] = useState(true);
|
|
34
|
-
const [autoTriggered, setAutoTriggered] = useState(false);
|
|
35
|
-
|
|
36
|
-
// Check WebAuthn support on mount
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
const checkSupport = async () => {
|
|
39
|
-
const supported = isWebAuthnSupported();
|
|
40
|
-
const platformAvailable = await isPlatformAuthenticatorAvailable();
|
|
41
|
-
setIsSupported(supported && platformAvailable);
|
|
42
|
-
|
|
43
|
-
if (!supported || !platformAvailable) {
|
|
44
|
-
setError(t('webauthn_not_supported', language));
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
checkSupport();
|
|
48
|
-
}, [language]);
|
|
49
|
-
|
|
50
|
-
// Auto-trigger passkey auth on mount (better UX)
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (isSupported && !autoTriggered && !previewMode) {
|
|
53
|
-
setAutoTriggered(true);
|
|
54
|
-
// Small delay to let the UI render first
|
|
55
|
-
const timer = setTimeout(() => {
|
|
56
|
-
handleAuthenticate();
|
|
57
|
-
}, 500);
|
|
58
|
-
return () => clearTimeout(timer);
|
|
59
|
-
}
|
|
60
|
-
}, [isSupported, autoTriggered, previewMode]);
|
|
61
|
-
|
|
62
|
-
const handleAuthenticate = async () => {
|
|
63
|
-
if (isLoading) return;
|
|
64
|
-
|
|
65
|
-
setIsLoading(true);
|
|
66
|
-
setError('');
|
|
67
|
-
|
|
68
|
-
if (previewMode) {
|
|
69
|
-
// Mock success for preview
|
|
70
|
-
setTimeout(() => {
|
|
71
|
-
setIsLoading(false);
|
|
72
|
-
onSuccess('mock-session-token', {
|
|
73
|
-
user_id: 'mock-user-id',
|
|
74
|
-
email: email,
|
|
75
|
-
tier: 'layer_1'
|
|
76
|
-
});
|
|
77
|
-
}, 1500);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
// Step 1: Get authentication options from server
|
|
83
|
-
const initResponse = await fetch(`${apiBaseUrl}/api/passkey/auth/begin`, {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: { 'Content-Type': 'application/json' },
|
|
86
|
-
credentials: 'include',
|
|
87
|
-
body: JSON.stringify({ email })
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (!initResponse.ok) {
|
|
91
|
-
const errorData = await initResponse.json().catch(() => ({}));
|
|
92
|
-
throw new Error(errorData.error || 'Failed to initialize authentication');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const initData: PasskeyAuthInitResponse = await initResponse.json();
|
|
96
|
-
|
|
97
|
-
if (!initData.success || !initData.options) {
|
|
98
|
-
throw new Error(initData.error || 'Invalid authentication options');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Step 2: Trigger WebAuthn authentication
|
|
102
|
-
const credential = await authenticateWithPasskey(initData.options);
|
|
103
|
-
|
|
104
|
-
// Step 3: Complete authentication with server
|
|
105
|
-
const completeResponse = await fetch(`${apiBaseUrl}/api/passkey/auth/finish`, {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: { 'Content-Type': 'application/json' },
|
|
108
|
-
credentials: 'include',
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
email,
|
|
111
|
-
credential
|
|
112
|
-
})
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (!completeResponse.ok) {
|
|
116
|
-
const errorData = await completeResponse.json().catch(() => ({}));
|
|
117
|
-
throw new Error(errorData.error || 'Authentication failed');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const completeData: PasskeyAuthCompleteResponse = await completeResponse.json();
|
|
121
|
-
|
|
122
|
-
if (completeData.success && completeData.session_token) {
|
|
123
|
-
onSuccess(completeData.session_token, completeData.user, completeData.business_token);
|
|
124
|
-
} else {
|
|
125
|
-
throw new Error(completeData.error || 'Authentication failed');
|
|
126
|
-
}
|
|
127
|
-
} catch (err) {
|
|
128
|
-
const message = err instanceof Error ? err.message : 'Authentication failed';
|
|
129
|
-
|
|
130
|
-
// Handle user cancellation gracefully
|
|
131
|
-
if (message.includes('cancelled') || message.includes('NotAllowedError')) {
|
|
132
|
-
setError(t('passkey_cancelled', language));
|
|
133
|
-
} else {
|
|
134
|
-
setError(message);
|
|
135
|
-
onError?.(message);
|
|
136
|
-
}
|
|
137
|
-
} finally {
|
|
138
|
-
setIsLoading(false);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<motion.div
|
|
144
|
-
initial={{ opacity: 0, x: 20 }}
|
|
145
|
-
animate={{ opacity: 1, x: 0 }}
|
|
146
|
-
exit={{ opacity: 0, x: -20 }}
|
|
147
|
-
className="flex-1 flex flex-col px-8 pt-12"
|
|
148
|
-
>
|
|
149
|
-
<div className="mb-8">
|
|
150
|
-
<h2 className="text-3xl font-extrabold text-main tracking-tight leading-tight">
|
|
151
|
-
{t('welcome_back', language)}
|
|
152
|
-
</h2>
|
|
153
|
-
<p className="text-muted mt-3 text-sm leading-relaxed">
|
|
154
|
-
{t('passkey_auth_subtitle', language)}
|
|
155
|
-
</p>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
{/* Email display */}
|
|
159
|
-
<div className="mb-6 p-4 dark:bg-white/5 bg-slate-100/50 rounded-2xl">
|
|
160
|
-
<p className="text-xs text-muted mb-1">{t('signing_in_as', language)}</p>
|
|
161
|
-
<p className="text-main font-semibold">{email}</p>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
{/* Passkey animation area */}
|
|
165
|
-
<div className="flex-1 flex flex-col items-center justify-center py-8">
|
|
166
|
-
<motion.div
|
|
167
|
-
animate={isLoading ? {
|
|
168
|
-
scale: [1, 1.1, 1],
|
|
169
|
-
opacity: [1, 0.7, 1]
|
|
170
|
-
} : {}}
|
|
171
|
-
transition={{
|
|
172
|
-
duration: 1.5,
|
|
173
|
-
repeat: isLoading ? Infinity : 0,
|
|
174
|
-
ease: "easeInOut"
|
|
175
|
-
}}
|
|
176
|
-
className={`w-24 h-24 rounded-full flex items-center justify-center mb-6 ${
|
|
177
|
-
isLoading
|
|
178
|
-
? 'bg-brand/20 border-2 border-brand'
|
|
179
|
-
: error
|
|
180
|
-
? 'bg-red-500/10 border-2 border-red-400'
|
|
181
|
-
: 'bg-brand/10 border-2 border-brand/30'
|
|
182
|
-
}`}
|
|
183
|
-
>
|
|
184
|
-
{isLoading ? (
|
|
185
|
-
<Loader2 className="w-10 h-10 text-brand animate-spin" />
|
|
186
|
-
) : error ? (
|
|
187
|
-
<AlertCircle className="w-10 h-10 text-red-400" />
|
|
188
|
-
) : (
|
|
189
|
-
<Fingerprint className="w-10 h-10 text-brand" />
|
|
190
|
-
)}
|
|
191
|
-
</motion.div>
|
|
192
|
-
|
|
193
|
-
<p className="text-main font-semibold text-center mb-2">
|
|
194
|
-
{isLoading
|
|
195
|
-
? t('authenticating', language)
|
|
196
|
-
: error
|
|
197
|
-
? t('authentication_failed', language)
|
|
198
|
-
: t('use_passkey', language)
|
|
199
|
-
}
|
|
200
|
-
</p>
|
|
201
|
-
|
|
202
|
-
{error && (
|
|
203
|
-
<p className="text-red-500 text-sm text-center mb-4 max-w-xs">
|
|
204
|
-
{error}
|
|
205
|
-
</p>
|
|
206
|
-
)}
|
|
207
|
-
|
|
208
|
-
{!isLoading && (
|
|
209
|
-
<p className="text-muted text-sm text-center max-w-xs">
|
|
210
|
-
{t('passkey_auth_hint', language)}
|
|
211
|
-
</p>
|
|
212
|
-
)}
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{/* Action buttons */}
|
|
216
|
-
<div className="space-y-3 pb-8">
|
|
217
|
-
{!isLoading && (
|
|
218
|
-
<button
|
|
219
|
-
onClick={handleAuthenticate}
|
|
220
|
-
disabled={!isSupported}
|
|
221
|
-
className="w-full h-14 bg-brand hover:bg-brand/90 text-white font-bold rounded-2xl shadow-xl shadow-brand/30 flex items-center justify-center gap-2 transition-transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
222
|
-
>
|
|
223
|
-
{error ? (
|
|
224
|
-
<>
|
|
225
|
-
<RefreshCw className="w-5 h-5" />
|
|
226
|
-
<span>{t('try_again', language)}</span>
|
|
227
|
-
</>
|
|
228
|
-
) : (
|
|
229
|
-
<>
|
|
230
|
-
<Fingerprint className="w-5 h-5" />
|
|
231
|
-
<span>{t('authenticate', language)}</span>
|
|
232
|
-
</>
|
|
233
|
-
)}
|
|
234
|
-
</button>
|
|
235
|
-
)}
|
|
236
|
-
|
|
237
|
-
{onFallbackToOTP && (
|
|
238
|
-
<button
|
|
239
|
-
onClick={onFallbackToOTP}
|
|
240
|
-
disabled={isLoading}
|
|
241
|
-
className="w-full h-12 bg-transparent border dark:border-white/10 border-slate-200 text-muted hover:text-main font-medium rounded-2xl flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
|
|
242
|
-
>
|
|
243
|
-
{t('use_email_instead', language)}
|
|
244
|
-
</button>
|
|
245
|
-
)}
|
|
246
|
-
</div>
|
|
247
|
-
|
|
248
|
-
{/* Security badge */}
|
|
249
|
-
<div className="flex items-center justify-center gap-2 pb-4 opacity-50">
|
|
250
|
-
<ShieldCheck className="text-muted w-3 h-3" />
|
|
251
|
-
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted">
|
|
252
|
-
{t('biometric_secured', language)}
|
|
253
|
-
</span>
|
|
254
|
-
</div>
|
|
255
|
-
</motion.div>
|
|
256
|
-
);
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
export default PasskeyAuth;
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { Fingerprint, Loader2, ShieldCheck, AlertCircle, CheckCircle2, Key, Smartphone, Lock } from 'lucide-react';
|
|
4
|
-
import { t } from '../utils/i18n';
|
|
5
|
-
import {
|
|
6
|
-
isWebAuthnSupported,
|
|
7
|
-
isPlatformAuthenticatorAvailable,
|
|
8
|
-
createPasskey
|
|
9
|
-
} from '../utils/webauthn';
|
|
10
|
-
import type { PasskeyRegisterInitResponse, PasskeyRegisterCompleteResponse } from '../types';
|
|
11
|
-
|
|
12
|
-
interface PasskeyRegisterProps {
|
|
13
|
-
email: string;
|
|
14
|
-
sessionToken: string;
|
|
15
|
-
onSuccess: (user: PasskeyRegisterCompleteResponse['user'], businessToken?: string) => void;
|
|
16
|
-
onError?: (error: string) => void;
|
|
17
|
-
onSkip?: () => void;
|
|
18
|
-
apiBaseUrl?: string;
|
|
19
|
-
previewMode?: boolean;
|
|
20
|
-
language?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const PasskeyRegister: React.FC<PasskeyRegisterProps> = ({
|
|
24
|
-
email,
|
|
25
|
-
sessionToken,
|
|
26
|
-
onSuccess,
|
|
27
|
-
onError,
|
|
28
|
-
onSkip,
|
|
29
|
-
apiBaseUrl = 'https://apis.casperid.com',
|
|
30
|
-
previewMode = false,
|
|
31
|
-
language = 'EN'
|
|
32
|
-
}) => {
|
|
33
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
34
|
-
const [error, setError] = useState('');
|
|
35
|
-
const [isSupported, setIsSupported] = useState(true);
|
|
36
|
-
const [isSuccess, setIsSuccess] = useState(false);
|
|
37
|
-
|
|
38
|
-
// Check WebAuthn support on mount
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
const checkSupport = async () => {
|
|
41
|
-
const supported = isWebAuthnSupported();
|
|
42
|
-
const platformAvailable = await isPlatformAuthenticatorAvailable();
|
|
43
|
-
setIsSupported(supported && platformAvailable);
|
|
44
|
-
|
|
45
|
-
if (!supported || !platformAvailable) {
|
|
46
|
-
setError(t('webauthn_not_supported', language));
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
checkSupport();
|
|
50
|
-
}, [language]);
|
|
51
|
-
|
|
52
|
-
const handleRegister = async () => {
|
|
53
|
-
if (isLoading) return;
|
|
54
|
-
|
|
55
|
-
setIsLoading(true);
|
|
56
|
-
setError('');
|
|
57
|
-
|
|
58
|
-
if (previewMode) {
|
|
59
|
-
// Mock success for preview
|
|
60
|
-
setTimeout(() => {
|
|
61
|
-
setIsLoading(false);
|
|
62
|
-
setIsSuccess(true);
|
|
63
|
-
setTimeout(() => {
|
|
64
|
-
onSuccess({
|
|
65
|
-
user_id: 'mock-user-id',
|
|
66
|
-
email: email,
|
|
67
|
-
tier: 'layer_0'
|
|
68
|
-
});
|
|
69
|
-
}, 1000);
|
|
70
|
-
}, 1500);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// Step 1: Get registration options from server
|
|
76
|
-
const initResponse = await fetch(`${apiBaseUrl}/api/passkey/register/begin`, {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers: {
|
|
79
|
-
'Content-Type': 'application/json',
|
|
80
|
-
'Authorization': `Bearer ${sessionToken}`
|
|
81
|
-
},
|
|
82
|
-
credentials: 'include',
|
|
83
|
-
body: JSON.stringify({ email })
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!initResponse.ok) {
|
|
87
|
-
const errorData = await initResponse.json().catch(() => ({}));
|
|
88
|
-
throw new Error(errorData.error || 'Failed to initialize registration');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const initData: PasskeyRegisterInitResponse = await initResponse.json();
|
|
92
|
-
|
|
93
|
-
if (!initData.success || !initData.options) {
|
|
94
|
-
throw new Error(initData.error || 'Invalid registration options');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Step 2: Create passkey using WebAuthn
|
|
98
|
-
const credential = await createPasskey(initData.options);
|
|
99
|
-
|
|
100
|
-
// Step 3: Complete registration with server
|
|
101
|
-
const completeResponse = await fetch(`${apiBaseUrl}/api/passkey/register/finish`, {
|
|
102
|
-
method: 'POST',
|
|
103
|
-
headers: {
|
|
104
|
-
'Content-Type': 'application/json',
|
|
105
|
-
'Authorization': `Bearer ${sessionToken}`
|
|
106
|
-
},
|
|
107
|
-
credentials: 'include',
|
|
108
|
-
body: JSON.stringify({
|
|
109
|
-
userId: initData.userId,
|
|
110
|
-
credential,
|
|
111
|
-
deviceInfo: {
|
|
112
|
-
userAgent: navigator.userAgent
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
if (!completeResponse.ok) {
|
|
118
|
-
const errorData = await completeResponse.json().catch(() => ({}));
|
|
119
|
-
throw new Error(errorData.error || 'Registration failed');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const completeData: PasskeyRegisterCompleteResponse = await completeResponse.json();
|
|
123
|
-
|
|
124
|
-
if (completeData.success) {
|
|
125
|
-
setIsSuccess(true);
|
|
126
|
-
// Brief delay to show success state
|
|
127
|
-
setTimeout(() => {
|
|
128
|
-
onSuccess(completeData.user, completeData.business_token);
|
|
129
|
-
}, 1000);
|
|
130
|
-
} else {
|
|
131
|
-
throw new Error(completeData.error || 'Registration failed');
|
|
132
|
-
}
|
|
133
|
-
} catch (err) {
|
|
134
|
-
const message = err instanceof Error ? err.message : 'Registration failed';
|
|
135
|
-
|
|
136
|
-
// Handle user cancellation gracefully
|
|
137
|
-
if (message.includes('cancelled') || message.includes('NotAllowedError')) {
|
|
138
|
-
setError(t('passkey_cancelled', language));
|
|
139
|
-
} else {
|
|
140
|
-
setError(message);
|
|
141
|
-
onError?.(message);
|
|
142
|
-
}
|
|
143
|
-
} finally {
|
|
144
|
-
setIsLoading(false);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const benefits = [
|
|
149
|
-
{ icon: Key, text: t('passkey_benefit_1', language) },
|
|
150
|
-
{ icon: ShieldCheck, text: t('passkey_benefit_2', language) },
|
|
151
|
-
{ icon: Smartphone, text: t('passkey_benefit_3', language) }
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<motion.div
|
|
156
|
-
initial={{ opacity: 0, x: 20 }}
|
|
157
|
-
animate={{ opacity: 1, x: 0 }}
|
|
158
|
-
exit={{ opacity: 0, x: -20 }}
|
|
159
|
-
className="flex-1 flex flex-col px-8 pt-12"
|
|
160
|
-
>
|
|
161
|
-
<div className="mb-8">
|
|
162
|
-
<h2 className="text-3xl font-extrabold text-main tracking-tight leading-tight">
|
|
163
|
-
{t('create_passkey', language)}
|
|
164
|
-
</h2>
|
|
165
|
-
<p className="text-muted mt-3 text-sm leading-relaxed">
|
|
166
|
-
{t('passkey_register_subtitle', language)}
|
|
167
|
-
</p>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
{/* Email display */}
|
|
171
|
-
<div className="mb-6 p-4 dark:bg-white/5 bg-slate-100/50 rounded-2xl">
|
|
172
|
-
<p className="text-xs text-muted mb-1">{t('signing_in_as', language)}</p>
|
|
173
|
-
<p className="text-main font-semibold">{email}</p>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{/* Benefits list */}
|
|
177
|
-
<div className="mb-8">
|
|
178
|
-
<p className="text-sm font-semibold text-main mb-4">{t('passkey_benefits_title', language)}</p>
|
|
179
|
-
<div className="space-y-3">
|
|
180
|
-
{benefits.map((benefit, index) => (
|
|
181
|
-
<div key={index} className="flex items-center gap-3">
|
|
182
|
-
<div className="w-10 h-10 rounded-xl bg-brand/10 flex items-center justify-center">
|
|
183
|
-
<benefit.icon className="w-5 h-5 text-brand" />
|
|
184
|
-
</div>
|
|
185
|
-
<span className="text-sm text-muted">{benefit.text}</span>
|
|
186
|
-
</div>
|
|
187
|
-
))}
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* Status indicator */}
|
|
192
|
-
{(isLoading || isSuccess || error) && (
|
|
193
|
-
<div className="flex-1 flex flex-col items-center justify-center py-4">
|
|
194
|
-
<motion.div
|
|
195
|
-
animate={isLoading ? {
|
|
196
|
-
scale: [1, 1.1, 1],
|
|
197
|
-
opacity: [1, 0.7, 1]
|
|
198
|
-
} : {}}
|
|
199
|
-
transition={{
|
|
200
|
-
duration: 1.5,
|
|
201
|
-
repeat: isLoading ? Infinity : 0,
|
|
202
|
-
ease: "easeInOut"
|
|
203
|
-
}}
|
|
204
|
-
className={`w-20 h-20 rounded-full flex items-center justify-center mb-4 ${
|
|
205
|
-
isSuccess
|
|
206
|
-
? 'bg-green-500/20 border-2 border-green-400'
|
|
207
|
-
: isLoading
|
|
208
|
-
? 'bg-brand/20 border-2 border-brand'
|
|
209
|
-
: 'bg-red-500/10 border-2 border-red-400'
|
|
210
|
-
}`}
|
|
211
|
-
>
|
|
212
|
-
{isSuccess ? (
|
|
213
|
-
<CheckCircle2 className="w-10 h-10 text-green-400" />
|
|
214
|
-
) : isLoading ? (
|
|
215
|
-
<Loader2 className="w-10 h-10 text-brand animate-spin" />
|
|
216
|
-
) : (
|
|
217
|
-
<AlertCircle className="w-10 h-10 text-red-400" />
|
|
218
|
-
)}
|
|
219
|
-
</motion.div>
|
|
220
|
-
|
|
221
|
-
<p className="text-main font-semibold text-center">
|
|
222
|
-
{isSuccess
|
|
223
|
-
? t('passkey_created', language)
|
|
224
|
-
: isLoading
|
|
225
|
-
? t('creating_passkey', language)
|
|
226
|
-
: t('passkey_create_failed', language)
|
|
227
|
-
}
|
|
228
|
-
</p>
|
|
229
|
-
|
|
230
|
-
{error && (
|
|
231
|
-
<p className="text-red-500 text-sm text-center mt-2 max-w-xs">
|
|
232
|
-
{error}
|
|
233
|
-
</p>
|
|
234
|
-
)}
|
|
235
|
-
</div>
|
|
236
|
-
)}
|
|
237
|
-
|
|
238
|
-
{/* Action buttons */}
|
|
239
|
-
<div className="mt-auto space-y-3 pb-8">
|
|
240
|
-
{!isSuccess && (
|
|
241
|
-
<button
|
|
242
|
-
onClick={handleRegister}
|
|
243
|
-
disabled={!isSupported || isLoading}
|
|
244
|
-
className="w-full h-14 bg-brand hover:bg-brand/90 text-white font-bold rounded-2xl shadow-xl shadow-brand/30 flex items-center justify-center gap-2 transition-transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
|
|
245
|
-
>
|
|
246
|
-
{isLoading ? (
|
|
247
|
-
<>
|
|
248
|
-
<Loader2 className="w-5 h-5 animate-spin" />
|
|
249
|
-
<span>{t('creating_passkey', language)}</span>
|
|
250
|
-
</>
|
|
251
|
-
) : (
|
|
252
|
-
<>
|
|
253
|
-
<Fingerprint className="w-5 h-5" />
|
|
254
|
-
<span>{t('register_passkey', language)}</span>
|
|
255
|
-
</>
|
|
256
|
-
)}
|
|
257
|
-
</button>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{onSkip && !isLoading && !isSuccess && (
|
|
261
|
-
<button
|
|
262
|
-
onClick={onSkip}
|
|
263
|
-
className="w-full h-12 bg-transparent border dark:border-white/10 border-slate-200 text-muted hover:text-main font-medium rounded-2xl flex items-center justify-center transition-colors"
|
|
264
|
-
>
|
|
265
|
-
{t('do_this_later', language)}
|
|
266
|
-
</button>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
{/* Security badge */}
|
|
271
|
-
<div className="flex items-center justify-center gap-2 pb-4 opacity-50">
|
|
272
|
-
<Lock className="text-muted w-3 h-3" />
|
|
273
|
-
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-muted">
|
|
274
|
-
{t('e2e_encrypted', language)}
|
|
275
|
-
</span>
|
|
276
|
-
</div>
|
|
277
|
-
</motion.div>
|
|
278
|
-
);
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
export default PasskeyRegister;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { motion } from 'framer-motion';
|
|
3
|
-
import { ShieldCheck, CheckCircle2, Verified } from 'lucide-react';
|
|
4
|
-
import { t } from '../utils/i18n';
|
|
5
|
-
|
|
6
|
-
interface PermissionsRequestProps {
|
|
7
|
-
appName?: string;
|
|
8
|
-
appLogo?: string;
|
|
9
|
-
onApprove: () => void;
|
|
10
|
-
onDeny: () => void;
|
|
11
|
-
language?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const permissions = [
|
|
15
|
-
'perm_identity',
|
|
16
|
-
'perm_profile',
|
|
17
|
-
'perm_kyc',
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const PermissionsRequest: React.FC<PermissionsRequestProps> = ({
|
|
21
|
-
appName = 'This App',
|
|
22
|
-
appLogo,
|
|
23
|
-
onApprove,
|
|
24
|
-
onDeny,
|
|
25
|
-
language = 'EN',
|
|
26
|
-
}) => (
|
|
27
|
-
<motion.div
|
|
28
|
-
initial={{ opacity: 0, y: 20 }}
|
|
29
|
-
animate={{ opacity: 1, y: 0 }}
|
|
30
|
-
exit={{ opacity: 0, y: -20 }}
|
|
31
|
-
className="flex-1 px-8 pt-8 flex flex-col items-center"
|
|
32
|
-
>
|
|
33
|
-
<div className="flex flex-col items-center gap-4 mb-10">
|
|
34
|
-
<div className="relative">
|
|
35
|
-
<div className="absolute inset-0 rounded-full shadow-[0_0_20px_rgba(134,81,225,0.3)] animate-pulse" />
|
|
36
|
-
<div className="relative w-24 h-24 rounded-full border-2 border-brand-40 p-1 dark:bg-black/50 bg-white">
|
|
37
|
-
{appLogo ? (
|
|
38
|
-
<div className="w-full h-full rounded-full overflow-hidden">
|
|
39
|
-
<img src={appLogo} alt={appName} className="w-full h-full object-cover" />
|
|
40
|
-
</div>
|
|
41
|
-
) : (
|
|
42
|
-
<div className="w-full h-full rounded-full bg-gradient-to-br from-emerald-500 to-teal-700 flex items-center justify-center text-white font-bold text-2xl">
|
|
43
|
-
{appName.slice(0, 2).toUpperCase()}
|
|
44
|
-
</div>
|
|
45
|
-
)}
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
<div className="text-center">
|
|
49
|
-
<h1 className="text-2xl font-bold tracking-tight text-main">{appName}</h1>
|
|
50
|
-
<p className="text-brand/70 text-sm font-medium mt-1 uppercase tracking-wider">{t('requesting_access', language)}</p>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div className="w-full space-y-3">
|
|
55
|
-
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-4 px-1">
|
|
56
|
-
{t('permission_request', language)}
|
|
57
|
-
</h3>
|
|
58
|
-
{permissions.map((perm) => (
|
|
59
|
-
<div
|
|
60
|
-
key={perm}
|
|
61
|
-
className="flex items-center gap-4 p-4 rounded-2xl dark:bg-white/5 bg-black/5 transition-all"
|
|
62
|
-
>
|
|
63
|
-
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-cyan-400/10 text-cyan-400 shrink-0">
|
|
64
|
-
<CheckCircle2 className="w-5 h-5" />
|
|
65
|
-
</div>
|
|
66
|
-
<p className="text-sm font-medium text-main">{t(perm, language)}</p>
|
|
67
|
-
</div>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
|
-
<div className="mt-8 flex gap-3 p-4 rounded-2xl dark:bg-brand/5 bg-brand/5">
|
|
72
|
-
<ShieldCheck className="text-brand w-5 h-5 shrink-0" />
|
|
73
|
-
<p className="text-xs leading-relaxed text-muted">
|
|
74
|
-
<span className="font-bold text-main">{t('privacy_note', language)}:</span> {t('privacy_desc', language)}
|
|
75
|
-
</p>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<footer className="mt-auto w-full pb-8 flex flex-col gap-3">
|
|
79
|
-
<button
|
|
80
|
-
onClick={onApprove}
|
|
81
|
-
className="w-full py-4 rounded-2xl bg-brand hover:bg-brand-90 text-white font-bold text-base shadow-xl shadow-brand transition-all flex items-center justify-center gap-2"
|
|
82
|
-
>
|
|
83
|
-
{t('approve_verify', language)}
|
|
84
|
-
<Verified className="w-5 h-5" />
|
|
85
|
-
</button>
|
|
86
|
-
<button
|
|
87
|
-
onClick={onDeny}
|
|
88
|
-
className="w-full py-3 rounded-2xl bg-transparent dark:hover:bg-white/5 hover:bg-black/5 dark:text-slate-400 text-slate-500 font-semibold text-sm transition-all"
|
|
89
|
-
>
|
|
90
|
-
{t('deny', language)}
|
|
91
|
-
</button>
|
|
92
|
-
</footer>
|
|
93
|
-
</motion.div>
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
export default PermissionsRequest;
|