@@ -8,13 +8,12 @@ import {
88 faExclamationCircle ,
99} from "@fortawesome/free-solid-svg-icons" ;
1010import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
11- import { useMutation } from "@tanstack/react-query" ;
1211import { AnimatePresence , motion } from "framer-motion" ;
1312import Cookies from "js-cookie" ;
1413import { LucideArrowUpRight } from "lucide-react" ;
1514import Image from "next/image" ;
1615import Link from "next/link" ;
17- import { useSearchParams } from "next/navigation" ;
16+ import { useRouter , useSearchParams } from "next/navigation" ;
1817import { signIn } from "next-auth/react" ;
1918import { Suspense , useEffect , useState } from "react" ;
2019import { toast } from "sonner" ;
@@ -28,46 +27,20 @@ const MotionButton = motion(Button);
2827
2928export function LoginForm ( ) {
3029 const searchParams = useSearchParams ( ) ;
30+ const router = useRouter ( ) ;
3131 const next = searchParams ?. get ( "next" ) ;
3232 const [ email , setEmail ] = useState ( "" ) ;
33+ const [ loading , setLoading ] = useState ( false ) ;
34+ const [ emailSent , setEmailSent ] = useState ( false ) ;
3335 const [ oauthError , setOauthError ] = useState ( false ) ;
3436 const [ showOrgInput , setShowOrgInput ] = useState ( false ) ;
3537 const [ organizationId , setOrganizationId ] = useState ( "" ) ;
3638 const [ organizationName , setOrganizationName ] = useState < string | null > ( null ) ;
39+ const [ lastEmailSentTime , setLastEmailSentTime ] = useState < number | null > (
40+ null ,
41+ ) ;
3742 const theme = Cookies . get ( "theme" ) || "light" ;
3843
39- const emailSignInMutation = useMutation ( {
40- mutationFn : async ( email : string ) => {
41- trackEvent ( "auth_started" , {
42- method : "email" ,
43- is_signup : ! oauthError ,
44- } ) ;
45-
46- const result = await signIn ( "email" , {
47- email,
48- redirect : false ,
49- ...( next && next . length > 0 ? { callbackUrl : next } : { } ) ,
50- } ) ;
51-
52- if ( ! result ?. ok || result ?. error ) {
53- throw new Error ( "Failed to send email" ) ;
54- }
55-
56- return result ;
57- } ,
58- onSuccess : ( ) => {
59- trackEvent ( "auth_email_sent" , {
60- email_domain : email . split ( "@" ) [ 1 ] ,
61- } ) ;
62- toast . success ( "Email sent - check your inbox!" ) ;
63- } ,
64- onError : ( ) => {
65- toast . error ( "Error sending email - try again?" ) ;
66- } ,
67- } ) ;
68-
69- const emailSent = emailSignInMutation . isSuccess ;
70-
7144 useEffect ( ( ) => {
7245 theme === "dark"
7346 ? ( document . body . className = "dark" )
@@ -123,8 +96,6 @@ export function LoginForm() {
12396 } ) ;
12497 const data = await response . json ( ) ;
12598
126- console . log ( data ) ;
127-
12899 if ( data . url ) {
129100 window . location . href = data . url ;
130101 }
@@ -274,7 +245,64 @@ export function LoginForm() {
274245 e . preventDefault ( ) ;
275246 if ( ! email ) return ;
276247
277- emailSignInMutation . mutate ( email ) ;
248+ // Check if we're rate limited on the client side
249+ if ( lastEmailSentTime ) {
250+ const timeSinceLastRequest =
251+ Date . now ( ) - lastEmailSentTime ;
252+ const waitTime = 30000 ; // 30 seconds
253+ if ( timeSinceLastRequest < waitTime ) {
254+ const remainingSeconds = Math . ceil (
255+ ( waitTime - timeSinceLastRequest ) / 1000 ,
256+ ) ;
257+ toast . error (
258+ `Please wait ${ remainingSeconds } seconds before requesting a new code` ,
259+ ) ;
260+ return ;
261+ }
262+ }
263+
264+ setLoading ( true ) ;
265+ trackEvent ( "auth_started" , {
266+ method : "email" ,
267+ is_signup : ! oauthError ,
268+ } ) ;
269+ signIn ( "email" , {
270+ email,
271+ redirect : false ,
272+ ...( next && next . length > 0
273+ ? { callbackUrl : next }
274+ : { } ) ,
275+ } )
276+ . then ( ( res ) => {
277+ setLoading ( false ) ;
278+
279+ if ( res ?. ok && ! res ?. error ) {
280+ setEmailSent ( true ) ;
281+ setLastEmailSentTime ( Date . now ( ) ) ;
282+ trackEvent ( "auth_email_sent" , {
283+ email_domain : email . split ( "@" ) [ 1 ] ,
284+ } ) ;
285+ const params = new URLSearchParams ( {
286+ email,
287+ ...( next && { next } ) ,
288+ lastSent : Date . now ( ) . toString ( ) ,
289+ } ) ;
290+ router . push ( `/verify-otp?${ params . toString ( ) } ` ) ;
291+ } else {
292+ // NextAuth always returns "EmailSignin" for all email provider errors
293+ // Since we already check rate limiting on the client side before sending,
294+ // if we get an error here, it's likely rate limiting from the server
295+ toast . error (
296+ "Please wait 30 seconds before requesting a new code" ,
297+ ) ;
298+ }
299+ } )
300+ . catch ( ( error ) => {
301+ setEmailSent ( false ) ;
302+ setLoading ( false ) ;
303+ // Catch block is rarely triggered with NextAuth
304+ toast . error ( "Error sending email - try again?" ) ;
305+ } ) ;
278306 } }
279307 className = "flex flex-col space-y-3"
280308 >
@@ -283,7 +311,7 @@ export function LoginForm() {
283311 email = { email }
284312 emailSent = { emailSent }
285313 setEmail = { setEmail }
286- loading = { emailSignInMutation . isPending }
314+ loading = { loading }
287315 oauthError = { oauthError }
288316 handleGoogleSignIn = { handleGoogleSignIn }
289317 />
@@ -320,8 +348,9 @@ export function LoginForm() {
320348 layout
321349 className = "pt-3 mx-auto text-sm underline text-gray-10 hover:text-gray-8"
322350 onClick = { ( ) => {
351+ setEmailSent ( false ) ;
323352 setEmail ( "" ) ;
324- emailSignInMutation . reset ( ) ;
353+ setLoading ( false ) ;
325354 } }
326355 >
327356 Click to restart sign in process
0 commit comments