33import { Button } from "@/components/ui/button" ;
44import { Card , CardContent , CardHeader , CardTitle } from "@/components/ui/card" ;
55import { Input } from "@/components/ui/input" ;
6+ import {
7+ InputOTP ,
8+ InputOTPGroup ,
9+ InputOTPSlot ,
10+ } from "@/components/ui/input-otp" ;
611import { Label } from "@/components/ui/label" ;
7- import { signIn } from "@/lib/betterauth/auth-client" ;
12+ import { authClient , signIn } from "@/lib/betterauth/auth-client" ;
813import { FingerprintIcon } from "lucide-react" ;
914import Image from "next/image" ;
1015import Link from "next/link" ;
@@ -15,8 +20,9 @@ import logo from "../../../public/images/logo.png";
1520
1621export default function SignInForm ( ) {
1722 const [ email , setEmail ] = useState ( "" ) ;
23+ const [ otp , setOtp ] = useState ( "" ) ;
24+ const [ hasSentEmail , setHasSentEmail ] = useState ( false ) ;
1825 const [ processing , setProcessing ] = useState ( false ) ;
19- const [ hasSendEmail , setHasSendEmail ] = useState ( false ) ;
2026
2127 const router = useRouter ( ) ;
2228
@@ -46,51 +52,50 @@ export default function SignInForm() {
4652 < CardTitle className = "text-hero text-2xl" > Get Started</ CardTitle >
4753 </ CardHeader >
4854
49- < CardContent className = "grid gap-4" >
50- < Label htmlFor = "email" > Email</ Label >
55+ { hasSentEmail ? (
56+ < CardContent className = "grid gap-4" >
57+ < Label > Login Code</ Label >
5158
52- < Input
53- id = "email"
54- type = "email"
55- placeholder = "m@example.com"
56- required
57- onChange = { ( e ) => {
58- setEmail ( e . target . value ) ;
59- } }
60- value = { email }
61- />
59+ < InputOTP
60+ maxLength = { 6 }
61+ onChange = { ( value ) => {
62+ setOtp ( value ) ;
63+ } }
64+ >
65+ < InputOTPGroup >
66+ < InputOTPSlot index = { 0 } />
67+ < InputOTPSlot index = { 1 } />
68+ < InputOTPSlot index = { 2 } />
69+ < InputOTPSlot index = { 3 } />
70+ < InputOTPSlot index = { 4 } />
71+ < InputOTPSlot index = { 5 } />
72+ </ InputOTPGroup >
73+ </ InputOTP >
6274
63- { hasSendEmail ? (
64- < p className = "text-sm text-gray-500 dark:text-gray-400" >
65- An email has been sent to{ " " }
66- < span className = "font-semibold" > { email } </ span > with a magic link
67- to sign in.
68- </ p >
69- ) : (
7075 < Button
7176 className = "gap-2"
7277 disabled = { processing }
7378 onClick = { async ( ) => {
7479 try {
75- if ( ! email ) return ;
80+ if ( ! otp || ! email ) return ;
7681 setProcessing ( true ) ;
7782 toast . promise (
7883 signIn
79- . magicLink ( { email, callbackURL : "/start" } )
84+ . emailOtp ( { email, otp } )
8085 . then ( ( result ) => {
8186 if ( result ?. error ) {
8287 throw new Error ( result . error ?. message ) ;
8388 }
8489
85- setHasSendEmail ( true ) ;
90+ router . push ( "/start" ) ;
8691 } )
8792 . finally ( ( ) => {
8893 setProcessing ( false ) ;
8994 } ) ,
9095 {
91- loading : "Sending magic link ..." ,
92- success : "Magic link sent !" ,
93- error : "Failed to send magic link ." ,
96+ loading : "Verifying your login code ..." ,
97+ success : "Login code verified !" ,
98+ error : "Failed to verify login code ." ,
9499 } ,
95100 ) ;
96101 } catch ( error ) {
@@ -100,41 +105,91 @@ export default function SignInForm() {
100105 }
101106 } }
102107 >
103- Sign-in with Magic Link
108+ Verify Login Code
104109 </ Button >
105- ) }
110+ </ CardContent >
111+ ) : (
112+ < CardContent className = "grid gap-4" >
113+ < Label htmlFor = "email" > Email</ Label >
106114
107- < Button
108- variant = "secondary"
109- className = "gap-2"
110- disabled = { processing }
111- onClick = { async ( ) => {
112- setProcessing ( true ) ;
113- toast . promise (
114- signIn
115- . passkey ( )
116- . then ( ( result ) => {
117- if ( result ?. error ) {
118- throw new Error ( result . error ?. message ) ;
119- }
115+ < Input
116+ id = "email"
117+ type = "email"
118+ placeholder = "m@example.com"
119+ required
120+ onChange = { ( e ) => {
121+ setEmail ( e . target . value ) ;
122+ } }
123+ value = { email }
124+ />
120125
121- router . push ( "/start" ) ;
122- } )
123- . finally ( ( ) => {
124- setProcessing ( false ) ;
125- } ) ,
126- {
127- loading : "Waiting for passkey..." ,
128- success : "Signed in with passkey!" ,
129- error : "Failed to receive passkey." ,
130- } ,
131- ) ;
132- } }
133- >
134- < FingerprintIcon size = { 16 } />
135- Sign-in with Passkey
136- </ Button >
137- </ CardContent >
126+ < Button
127+ className = "gap-2"
128+ disabled = { processing }
129+ onClick = { async ( ) => {
130+ try {
131+ if ( ! email ) return ;
132+ setProcessing ( true ) ;
133+ toast . promise (
134+ authClient . emailOtp
135+ . sendVerificationOtp ( { email, type : "sign-in" } )
136+ . then ( ( result ) => {
137+ if ( result ?. error ) {
138+ throw new Error ( result . error ?. message ) ;
139+ }
140+ setHasSentEmail ( true ) ;
141+ } )
142+ . finally ( ( ) => {
143+ setProcessing ( false ) ;
144+ } ) ,
145+ {
146+ loading : "Sending your login code..." ,
147+ success : "Login code sent!" ,
148+ error : "Failed to send login code." ,
149+ } ,
150+ ) ;
151+ } catch ( error ) {
152+ console . error ( error ) ;
153+ } finally {
154+ setProcessing ( false ) ;
155+ }
156+ } }
157+ >
158+ Sign-in with Email
159+ </ Button >
160+
161+ < Button
162+ variant = "secondary"
163+ className = "gap-2"
164+ disabled = { processing }
165+ onClick = { async ( ) => {
166+ setProcessing ( true ) ;
167+ toast . promise (
168+ signIn
169+ . passkey ( )
170+ . then ( ( result ) => {
171+ if ( result ?. error ) {
172+ throw new Error ( result . error ?. message ) ;
173+ }
174+
175+ router . push ( "/start" ) ;
176+ } )
177+ . finally ( ( ) => {
178+ setProcessing ( false ) ;
179+ } ) ,
180+ {
181+ loading : "Waiting for passkey..." ,
182+ success : "Signed in with passkey!" ,
183+ error : "Failed to receive passkey." ,
184+ } ,
185+ ) ;
186+ } }
187+ >
188+ < FingerprintIcon size = { 16 } />
189+ Sign-in with Passkey
190+ </ Button >
191+ </ CardContent >
192+ ) }
138193 </ Card >
139194 </ div >
140195 ) ;
0 commit comments