@@ -22,21 +22,42 @@ import { cn } from '@/lib/utils';
2222import { yupResolver } from '@hookform/resolvers/yup' ;
2323import { startAuthentication } from '@simplewebauthn/browser' ;
2424import axios from 'axios' ;
25- import { KeyRound , Loader2 } from 'lucide-react' ;
26- import { useContext , useState } from 'react' ;
25+ import { KeyRound , Loader2 , AlertCircle , ShieldCheck } from 'lucide-react' ;
26+ import { useContext , useState , useEffect } from 'react' ;
2727import { useForm } from 'react-hook-form' ;
2828import { useNavigate } from 'react-router-dom' ;
2929import { toast } from 'sonner' ;
3030import * as yup from 'yup' ;
3131import DotPattern from '../ui/dot-pattern' ;
32- import { useEffect } from 'react' ;
3332
3433const schema = yup
3534 . object ( {
3635 email : yup . string ( ) . email ( 'Please enter a valid email' ) . required ( 'Email is required' ) ,
3736 } )
3837 . required ( ) ;
3938
39+ // WebAuthn error messages for better UX
40+ const getWebAuthnErrorMessage = ( error ) => {
41+ const name = error ?. name || '' ;
42+ const message = error ?. message || '' ;
43+
44+ switch ( name ) {
45+ case 'NotAllowedError' :
46+ if ( message . includes ( 'timed out' ) ) {
47+ return 'Authentication timed out. Please try again.' ;
48+ }
49+ return 'Authentication was cancelled or not allowed. Please try again.' ;
50+ case 'SecurityError' :
51+ return 'Security error. Make sure you are using HTTPS.' ;
52+ case 'NotSupportedError' :
53+ return 'Passkeys are not supported on this device or browser.' ;
54+ case 'AbortError' :
55+ return 'The operation was cancelled.' ;
56+ default :
57+ return null ; // Return null for API errors to use the server message
58+ }
59+ } ;
60+
4061export const PasskeyLogin = ( ) => {
4162 const { getProfile } = useContext ( Context ) ;
4263 const navigate = useNavigate ( ) ;
@@ -64,7 +85,8 @@ export const PasskeyLogin = () => {
6485 setUserData ( data . data ) ;
6586 }
6687 } catch ( error ) {
67- console . log ( error ) ;
88+ // Silently fail - IP info is optional
89+ console . debug ( 'Failed to fetch IP info:' , error ) ;
6890 }
6991 } ;
7092
@@ -73,69 +95,72 @@ export const PasskeyLogin = () => {
7395 setError ( '' ) ;
7496
7597 try {
98+ // Step 1: Get authentication options from server
7699 const options = await axios . post (
77100 `${ import . meta. env . VITE_API_URL } /api/auth/passkey/authenticate` ,
101+ { email : data . email }
102+ ) ;
103+
104+ if ( ! options . data ) {
105+ throw new Error ( 'Failed to get authentication options' ) ;
106+ }
107+
108+ // Step 2: Authenticate using WebAuthn API
109+ const assertionResponse = await startAuthentication ( {
110+ optionsJSON : options . data ,
111+ } ) ;
112+
113+ // Step 3: Verify with server
114+ const verificationRes = await axios . post (
115+ `${ import . meta. env . VITE_API_URL } /api/auth/passkey/authenticate/verify` ,
78116 {
117+ assertionResponse : assertionResponse ,
79118 email : data . email ,
80- }
119+ userData : userData ,
120+ } ,
121+ { withCredentials : true }
81122 ) ;
82123
83- if ( options . data ) {
84- const assertionResponse = await startAuthentication ( {
85- optionsJSON : options . data ,
86- } ) ;
87-
88- const verificationRes = await axios . post (
89- `${ import . meta. env . VITE_API_URL } /api/auth/passkey/authenticate/verify` ,
90- {
91- assertionResponse : assertionResponse ,
92- email : data . email ,
93- userData : userData ,
94- } ,
95- {
96- withCredentials : true ,
97- }
98- ) ;
99-
100- const verificationResult = verificationRes . data ;
101-
102- if ( verificationResult . verified ) {
103- await getProfile ( ) ;
104- toast . success ( 'Login successful' ) ;
105- navigate ( '/feed' ) ;
106- }
124+ if ( verificationRes . data . verified ) {
125+ await getProfile ( ) ;
126+ toast . success ( 'Login successful!' ) ;
127+ navigate ( '/feed' ) ;
128+ } else {
129+ setError ( 'Authentication verification failed. Please try again.' ) ;
107130 }
108131 } catch ( error ) {
109- if ( error ?. response ?. data ?. message ) {
132+ console . error ( 'Passkey login error:' , error ) ;
133+
134+ // Check for WebAuthn-specific errors first
135+ const webAuthnError = getWebAuthnErrorMessage ( error ) ;
136+ if ( webAuthnError ) {
137+ setError ( webAuthnError ) ;
138+ } else if ( error ?. response ?. data ?. message ) {
110139 setError ( error . response . data . message ) ;
140+ } else {
141+ setError ( 'An unexpected error occurred. Please try again.' ) ;
111142 }
112143 } finally {
113144 setIsLoading ( false ) ;
114145 }
115146 } ;
116147
117148 return (
118- < div className = 'flex items-center justify-center max-h-svh h-svh overflow-hidden relative' >
119- < DotPattern
120- className = { cn ( '[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]' ) }
121- />
122- < div className = 'absolute -top-24 -left-24 w-96 h-96 rounded-full bg-purple-600/30 filter blur-[100px] animate-pulse' > </ div >
123- < div
124- className = 'absolute -bottom-32 -right-32 w-96 h-96 rounded-full bg-pink-600/20 filter blur-[120px] animate-pulse'
125- style = { { animationDelay : '2s' } }
126- > </ div >
127- < div
128- className = 'absolute top-1/3 right-1/4 w-64 h-64 rounded-full bg-blue-600/20 filter blur-[80px] animate-pulse'
129- style = { { animationDelay : '1s' } }
130- > </ div >
131- < Card className = 'w-full max-w-md mx-auto z-10' >
149+ < div className = 'flex items-center justify-center max-h-svh h-svh overflow-hidden relative bg-[#050505]' >
150+ { /* Subtle glow */ }
151+ < div className = 'absolute top-1/4 left-1/4 w-96 h-96 bg-emerald-500/5 rounded-full blur-[120px]' />
152+ < div className = 'absolute bottom-1/4 right-1/4 w-96 h-96 bg-teal-500/5 rounded-full blur-[120px]' />
153+ < Card className = 'w-full max-w-md mx-auto z-10 bg-white/[0.03] border-white/[0.08] backdrop-blur-sm' >
132154 < CardHeader className = 'flex flex-col gap-3' >
133155 < div className = 'flex items-center gap-2' >
134- < KeyRound className = 'w-6 h-6 text-primary' />
156+ < div className = 'p-2 rounded-lg bg-primary/10' >
157+ < KeyRound className = 'w-6 h-6 text-primary' />
158+ </ div >
135159 < CardTitle > Login with Passkey</ CardTitle >
136160 </ div >
137161 < CardDescription className = 'mt-2' >
138- Use your device's biometric authentication or security key to sign in
162+ Use your device's biometric authentication (Face ID, Touch ID, Windows Hello) or
163+ security key to sign in securely.
139164 </ CardDescription >
140165 </ CardHeader >
141166 < CardContent >
@@ -148,7 +173,12 @@ export const PasskeyLogin = () => {
148173 < FormItem >
149174 < FormLabel > Email</ FormLabel >
150175 < FormControl >
151- < Input placeholder = 'Enter your email' type = 'email' { ...field } />
176+ < Input
177+ placeholder = 'Enter your email'
178+ type = 'email'
179+ autoComplete = 'email webauthn'
180+ { ...field }
181+ />
152182 </ FormControl >
153183 < FormMessage />
154184 </ FormItem >
@@ -157,20 +187,41 @@ export const PasskeyLogin = () => {
157187
158188 { error && (
159189 < Alert variant = 'destructive' >
190+ < AlertCircle className = 'h-4 w-4' />
160191 < AlertDescription > { error } </ AlertDescription >
161192 </ Alert >
162193 ) }
163194
164- < Button type = 'submit' className = 'w-full' disabled = { isLoading } >
165- { isLoading && < Loader2 className = 'w-6 h-6 mr-2 animate-spin' /> }
166- { isLoading ? 'Authenticating...' : 'Login with Passkey' }
167- </ Button >
168- < Button variant = 'ghost' className = 'w-full' onClick = { ( ) => navigate ( '/login' ) } >
169- Login with Password
170- </ Button >
195+ < div className = 'space-y-3' >
196+ < Button type = 'submit' className = 'w-full' disabled = { isLoading } >
197+ { isLoading ? (
198+ < >
199+ < Loader2 className = 'w-5 h-5 mr-2 animate-spin' />
200+ Authenticating...
201+ </ >
202+ ) : (
203+ < >
204+ < ShieldCheck className = 'w-5 h-5 mr-2' />
205+ Continue with Passkey
206+ </ >
207+ ) }
208+ </ Button >
209+ < Button
210+ type = 'button'
211+ variant = 'ghost'
212+ className = 'w-full'
213+ onClick = { ( ) => navigate ( '/login' ) }
214+ >
215+ Login with Password
216+ </ Button >
217+ </ div >
171218 </ form >
172219 </ Form >
173- < CardFooter className = 'flex flex-col mt-3' > </ CardFooter >
220+ < CardFooter className = 'flex flex-col mt-3 px-0' >
221+ < p className = 'text-xs text-muted-foreground text-center' >
222+ Passkeys provide a more secure, phishing-resistant way to sign in without passwords.
223+ </ p >
224+ </ CardFooter >
174225 </ CardContent >
175226 </ Card >
176227 </ div >
0 commit comments