@@ -10,7 +10,6 @@ import {
10
10
import { Button } from '@/src/components/shadcn-ui/button' ;
11
11
import { Separator } from '@/src/components/shadcn-ui/separator' ;
12
12
import { Badge } from '@/src/components/shadcn-ui/badge' ;
13
- import { PasskeyLoginButton } from './_components/passkey-login' ;
14
13
import { At , Lock } from '@phosphor-icons/react' ;
15
14
import Link from 'next/link' ;
16
15
import Image from 'next/image' ;
@@ -25,24 +24,27 @@ import {
25
24
TurnstileComponent ,
26
25
turnstileEnabled
27
26
} from '@/src/components/turnstile' ;
28
- import { platform } from '@/src/lib/trpc' ;
29
- import { useState } from 'react' ;
30
27
import { TwoFactorDialog } from './_components/two-factor-dialog' ;
28
+ import { Fingerprint } from '@phosphor-icons/react' ;
29
+ import { platform } from '@/src/lib/trpc' ;
30
+ import { startAuthentication } from '@simplewebauthn/browser' ;
31
+ import { toast } from 'sonner' ;
32
+ import { useCallback , useState } from 'react' ;
31
33
32
34
const loginSchema = z . object ( {
33
35
username : zodSchemas . username ( 2 ) ,
34
- password : z . string ( ) . min ( 8 , 'Password must be at least 8 characters' ) ,
35
- turnstileToken : turnstileEnabled
36
- ? z . string ( {
37
- required_error :
38
- 'Waiting for Captcha. If you can see the Captcha, complete it manually'
39
- } )
40
- : z . undefined ( )
36
+ password : z . string ( ) . min ( 8 , 'Password must be at least 8 characters' )
41
37
} ) ;
42
38
43
39
export default function Page ( ) {
44
40
const router = useRouter ( ) ;
45
41
const [ twoFactorDialogOpen , setTwoFactorDialogOpen ] = useState ( false ) ;
42
+ const [ turnstileToken , setTurnstileToken ] = useState < string | undefined > ( ) ;
43
+ const generatePasskey =
44
+ platform . useUtils ( ) . auth . passkey . generatePasskeyChallenge ;
45
+ const { mutateAsync : loginPasskey } =
46
+ platform . auth . passkey . verifyPasskey . useMutation ( ) ;
47
+ const [ loadingPasskey , setLoadingPasskey ] = useState ( false ) ;
46
48
47
49
const form = useForm < z . infer < typeof loginSchema > > ( {
48
50
resolver : zodResolver ( loginSchema ) ,
@@ -53,29 +55,88 @@ export default function Page() {
53
55
} ) ;
54
56
55
57
const {
56
- mutateAsync : login ,
58
+ mutateAsync : loginPassword ,
57
59
error,
58
60
isPending
59
61
} = platform . auth . password . signIn . useMutation ( ) ;
60
62
61
- const handleLogin = async ( values : z . infer < typeof loginSchema > ) => {
63
+ const loginWithPasskey = useCallback ( async ( ) => {
64
+ if ( turnstileEnabled && ! turnstileToken ) {
65
+ toast . error (
66
+ 'Captcha has not been completed yet, if you can see the Captcha, complete it manually'
67
+ ) ;
68
+ return ;
69
+ }
62
70
try {
63
- const { status, defaultOrgShortcode } = await login ( values ) ;
64
- if ( status === 'NO_2FA_SETUP' ) {
65
- if ( ! defaultOrgShortcode ) {
66
- router . push ( '/join/org' ) ;
71
+ setLoadingPasskey ( true ) ;
72
+ const data = await generatePasskey . fetch ( {
73
+ turnstileToken
74
+ } ) ;
75
+ const response = await startAuthentication ( data . options ) ;
76
+ const { defaultOrg } = await loginPasskey ( {
77
+ verificationResponseRaw : response
78
+ } ) ;
79
+
80
+ if ( ! defaultOrg ) {
81
+ toast . error ( 'You are not a member of any organization' , {
82
+ description : 'Redirecting you to create an organization'
83
+ } ) ;
84
+ router . push ( '/join/org' ) ;
85
+ return ;
86
+ }
87
+
88
+ toast . success ( 'Sign in successful!' , {
89
+ description : 'Redirecting you to your conversations'
90
+ } ) ;
91
+ router . push ( `/${ defaultOrg } /convo` ) ;
92
+ } catch ( error ) {
93
+ if ( error instanceof Error ) {
94
+ if ( error . name === 'NotAllowedError' ) {
95
+ toast . warning ( 'Passkey login either timed out or was cancelled' ) ;
67
96
} else {
68
- router . push (
69
- `/ ${ defaultOrgShortcode } /settings/user/security?code=NO_2FA_SETUP`
70
- ) ;
97
+ toast . error ( error . name , {
98
+ description : error . message
99
+ } ) ;
71
100
}
72
101
} else {
73
- setTwoFactorDialogOpen ( true ) ;
102
+ console . error ( error ) ;
74
103
}
75
- } catch {
76
- /* do nothing */
104
+ } finally {
105
+ setLoadingPasskey ( false ) ;
77
106
}
78
- } ;
107
+ } , [ generatePasskey , router , turnstileToken , loginPasskey ] ) ;
108
+
109
+ const loginWithPassword = useCallback (
110
+ async ( { username, password } : z . infer < typeof loginSchema > ) => {
111
+ if ( turnstileEnabled && ! turnstileToken ) {
112
+ toast . error (
113
+ 'Captcha has not been completed yet, if you can see the Captcha, complete it manually'
114
+ ) ;
115
+ return ;
116
+ }
117
+ try {
118
+ const { status, defaultOrgShortcode } = await loginPassword ( {
119
+ username,
120
+ password,
121
+ turnstileToken
122
+ } ) ;
123
+ if ( status === 'NO_2FA_SETUP' ) {
124
+ if ( ! defaultOrgShortcode ) {
125
+ router . push ( '/join/org' ) ;
126
+ } else {
127
+ router . push (
128
+ `/${ defaultOrgShortcode } /settings/user/security?code=NO_2FA_SETUP`
129
+ ) ;
130
+ }
131
+ } else {
132
+ setTwoFactorDialogOpen ( true ) ;
133
+ }
134
+ } catch {
135
+ /* do nothing */
136
+ }
137
+ } ,
138
+ [ loginPassword , router , turnstileToken ]
139
+ ) ;
79
140
80
141
return (
81
142
< div className = "bg-base-2 flex h-full items-center justify-center" >
@@ -90,7 +151,16 @@ export default function Page() {
90
151
< h1 className = "mb-2 text-2xl font-medium" > Login to your UnInbox</ h1 >
91
152
< h2 className = "text-base-10 text-sm" > Enter your details to login.</ h2 >
92
153
< div className = "py-6" >
93
- < PasskeyLoginButton />
154
+ < div className = "flex flex-col gap-2" >
155
+ < Button
156
+ onClick = { ( ) => loginWithPasskey ( ) }
157
+ loading = { loadingPasskey }
158
+ disabled = { loadingPasskey || ( turnstileEnabled && ! turnstileToken ) }
159
+ className = "mb-2 w-72 cursor-pointer gap-2 font-semibold" >
160
+ < Fingerprint size = { 20 } />
161
+ < span > Login with my passkey</ span >
162
+ </ Button >
163
+ </ div >
94
164
< div className = "flex items-center justify-center gap-2 py-4" >
95
165
< Separator className = "bg-base-5 w-28" />
96
166
< Badge
@@ -103,7 +173,7 @@ export default function Page() {
103
173
< div className = "flex flex-col items-center justify-center gap-4 py-2" >
104
174
< Form { ...form } >
105
175
< form
106
- onSubmit = { form . handleSubmit ( handleLogin ) }
176
+ onSubmit = { form . handleSubmit ( loginWithPassword ) }
107
177
className = "flex w-full flex-col items-center justify-center gap-3" >
108
178
< FormField
109
179
control = { form . control }
@@ -143,22 +213,6 @@ export default function Page() {
143
213
< div className = "text-red-10 text-sm" > { error . message } </ div >
144
214
) }
145
215
146
- < FormField
147
- control = { form . control }
148
- name = "turnstileToken"
149
- render = { ( ) => (
150
- < FormItem >
151
- < FormControl >
152
- < TurnstileComponent
153
- onSuccess = { ( value ) =>
154
- form . setValue ( 'turnstileToken' , value )
155
- }
156
- />
157
- </ FormControl >
158
- < FormMessage />
159
- </ FormItem >
160
- ) }
161
- />
162
216
< Button
163
217
className = "w-full"
164
218
loading = { isPending }
@@ -168,6 +222,7 @@ export default function Page() {
168
222
</ form >
169
223
</ Form >
170
224
</ div >
225
+ < TurnstileComponent onSuccess = { setTurnstileToken } />
171
226
</ div >
172
227
< div className = "text-base-10 flex flex-col gap-2 text-sm" >
173
228
< Link
0 commit comments