@@ -10,7 +10,7 @@ import {
1010import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
1111import { AnimatePresence , motion } from "framer-motion" ;
1212import Cookies from "js-cookie" ;
13- import { LucideArrowUpRight } from "lucide-react" ;
13+ import { KeyRound , LucideArrowUpRight } from "lucide-react" ;
1414import Image from "next/image" ;
1515import Link from "next/link" ;
1616import { useRouter , useSearchParams } from "next/navigation" ;
@@ -40,6 +40,8 @@ export function LoginForm() {
4040 const [ lastEmailSentTime , setLastEmailSentTime ] = useState < number | null > (
4141 null ,
4242 ) ;
43+ const [ password , setPassword ] = useState ( "" ) ;
44+ const [ showCredientialLogin , setShowCredientialLogin ] = useState ( false ) ;
4345 const theme = Cookies . get ( "theme" ) || "light" ;
4446
4547 useEffect ( ( ) => {
@@ -248,6 +250,60 @@ export function LoginForm() {
248250 e . preventDefault ( ) ;
249251 if ( ! email ) return ;
250252
253+ if ( showCredientialLogin ) {
254+ if ( ! email || ! password ) {
255+ toast . error ( "Please enter email and password" ) ;
256+ return ;
257+ }
258+
259+ try {
260+ setLoading ( true ) ;
261+ trackEvent ( "auth_started" , { method : "password" , is_signup : false } ) ;
262+
263+ const res = await signIn ( "credentials" , {
264+ email,
265+ password,
266+ redirect : false ,
267+ ...( next && next . length > 0 ? { callbackUrl : next } : { } ) ,
268+ } ) ;
269+
270+ setLoading ( false ) ;
271+
272+ if ( res ?. ok && ! res ?. error ) {
273+ trackEvent ( "auth_success" , { method : "password" , is_signup : false } ) ;
274+ router . push ( next || "/" ) ;
275+ return ;
276+ }
277+
278+
279+ // Handle specific known errors first
280+ if ( res ?. error ?. toLowerCase ( ) . includes ( "verify" ) ) {
281+ toast . error ( "Please verify your email before logging in." ) ;
282+ const res = await fetch ( "/api/signup/resend" , {
283+ method : "POST" ,
284+ headers : { "Content-Type" : "application/json" } ,
285+ body : JSON . stringify ( { email } ) ,
286+ } ) ;
287+
288+ const data = await res . json ( ) ;
289+ if ( ! data ?. status ) {
290+ throw "Something went wrong,try other login methods" ;
291+ }
292+ router . push ( `/verify-otp?email=${ encodeURIComponent ( email ) } &type=credientials` ) ;
293+ return ;
294+ }
295+
296+ // Otherwise generic error
297+ toast . error ( "Invalid credentials – try again?" ) ;
298+ } catch ( error ) {
299+ console . error ( "Credential login error:" , error ) ;
300+ toast . error ( "Invalid credentials – try again?" ) ;
301+ } finally {
302+ setLoading ( false ) ;
303+ }
304+ return ;
305+ }
306+
251307 // Check if we're rate limited on the client side
252308 if ( lastEmailSentTime ) {
253309 const timeSinceLastRequest =
@@ -309,15 +365,28 @@ export function LoginForm() {
309365 } }
310366 className = "flex flex-col space-y-3"
311367 >
312- < NormalLogin
313- setShowOrgInput = { setShowOrgInput }
314- email = { email }
315- emailSent = { emailSent }
316- setEmail = { setEmail }
317- loading = { loading }
318- oauthError = { oauthError }
319- handleGoogleSignIn = { handleGoogleSignIn }
320- />
368+ { showCredientialLogin ? (
369+ < LoginWithEmailAndPassword
370+ email = { email }
371+ emailSent = { emailSent }
372+ setEmail = { setEmail }
373+ password = { password }
374+ setPassword = { setPassword }
375+ loading = { loading }
376+ setShowCredientialLogin = { setShowCredientialLogin }
377+ />
378+ ) : (
379+ < NormalLogin
380+ setShowOrgInput = { setShowOrgInput }
381+ email = { email }
382+ emailSent = { emailSent }
383+ setEmail = { setEmail }
384+ loading = { loading }
385+ oauthError = { oauthError }
386+ handleGoogleSignIn = { handleGoogleSignIn }
387+ setShowCredientialLogin = { setShowCredientialLogin }
388+ />
389+ ) }
321390 </ motion . form >
322391 ) }
323392 </ motion . div >
@@ -388,6 +457,79 @@ const LoginWithSSO = ({
388457 ) ;
389458} ;
390459
460+ const LoginWithEmailAndPassword = ( {
461+ email,
462+ emailSent,
463+ setEmail,
464+ loading,
465+ password,
466+ setPassword,
467+ setShowCredientialLogin
468+ } : {
469+ email : string ;
470+ emailSent : boolean ;
471+ setEmail : ( email : string ) => void ;
472+ password : string ,
473+ setPassword : ( password : string ) => void ;
474+ loading : boolean ;
475+ setShowCredientialLogin : ( show : boolean ) => void
476+
477+ } ) => {
478+ return (
479+ < motion . div >
480+ < motion . div layout className = "flex flex-col space-y-3" >
481+ < MotionInput
482+ id = "email"
483+ name = "email"
484+ autoFocus
485+ type = "email"
486+ placeholder = { emailSent ? "" : "tim@apple.com" }
487+ autoComplete = "email"
488+ required
489+ value = { email }
490+ disabled = { emailSent || loading }
491+ onChange = { ( e ) => {
492+ setEmail ( e . target . value ) ;
493+ } }
494+ />
495+ < MotionInput
496+ id = "password"
497+ name = "password"
498+ autoFocus
499+ type = "password"
500+ placeholder = { emailSent ? "" : "password" }
501+ autoComplete = "password"
502+ required
503+ value = { password }
504+ disabled = { emailSent || loading }
505+ onChange = { ( e ) => {
506+ setPassword ( e . target . value ) ;
507+ } }
508+ />
509+ < MotionButton
510+ variant = "dark"
511+ type = "submit"
512+ disabled = { loading || emailSent }
513+ icon = { < FontAwesomeIcon className = "mr-1 size-4" icon = { faEnvelope } /> }
514+ >
515+ Login with email
516+ </ MotionButton >
517+ < MotionButton
518+ variant = "gray"
519+ type = "button"
520+ className = "w-full"
521+ layout
522+ onClick = { ( ) => setShowCredientialLogin ( false ) }
523+ disabled = { loading || emailSent }
524+ >
525+ < LucideArrowUpRight size = { 20 } />
526+ Login with OTP
527+ </ MotionButton >
528+ </ motion . div >
529+ </ motion . div >
530+ ) ;
531+ } ;
532+
391533const NormalLogin = ( {
392534 setShowOrgInput,
393535 email,
@@ -396,6 +538,7 @@ const NormalLogin = ({
396538 loading,
397539 oauthError,
398540 handleGoogleSignIn,
541+ setShowCredientialLogin
399542} : {
400543 setShowOrgInput : ( show : boolean ) => void ;
401544 email : string ;
@@ -404,6 +547,7 @@ const NormalLogin = ({
404547 loading : boolean ;
405548 oauthError : boolean ;
406549 handleGoogleSignIn : ( ) => void ;
550+ setShowCredientialLogin : ( show : boolean ) => void
407551} ) => {
408552 const publicEnv = usePublicEnv ( ) ;
409553
@@ -455,7 +599,17 @@ const NormalLogin = ({
455599 Sign up here
456600 </ Link >
457601 </ motion . p >
458-
602+ < MotionButton
603+ variant = "gray"
604+ type = "button"
605+ className = "w-full"
606+ layout
607+ onClick = { ( ) => setShowCredientialLogin ( true ) }
608+ disabled = { loading || emailSent }
609+ >
610+ < KeyRound size = { 18 } />
611+ Login with Password
612+ </ MotionButton >
459613 { ( publicEnv . googleAuthAvailable || publicEnv . workosAuthAvailable ) && (
460614 < >
461615 < div className = "flex gap-4 items-center mt-4 mb-4" >
0 commit comments