@@ -5,7 +5,7 @@ import { t } from "@lingui/core/macro";
55import { Trans } from "@lingui/react/macro" ;
66import { useQuery } from "@tanstack/react-query" ;
77import { env } from "next-runtime-env" ;
8- import { useEffect , useState } from "react" ;
8+ import { useEffect , useMemo , useRef , useState } from "react" ;
99import { useForm } from "react-hook-form" ;
1010import {
1111 FaApple ,
@@ -156,10 +156,12 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
156156 const [ isLoginWithProviderPending , setIsLoginWithProviderPending ] =
157157 useState < null | AuthProvider > ( null ) ;
158158 const [ isCredentialsEnabled , setIsCredentialsEnabled ] = useState ( false ) ;
159+ const [ isEmailSendingEnabled , setIsEmailSendingEnabled ] = useState ( false ) ;
159160 const [ isLoginWithEmailPending , setIsLoginWithEmailPending ] = useState ( false ) ;
160161 const [ loginError , setLoginError ] = useState < string | null > ( null ) ;
161162 const { showPopup } = usePopup ( ) ;
162163 const oidcProviderName = "OIDC" ;
164+ const passwordRef = useRef < HTMLInputElement | null > ( null ) ;
163165
164166 const redirect = useSearchParams ( ) . get ( "next" ) ;
165167 const callbackURL = redirect ?? "/boards" ;
@@ -168,6 +170,9 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
168170 useEffect ( ( ) => {
169171 const credentialsAllowed =
170172 env ( "NEXT_PUBLIC_ALLOW_CREDENTIALS" ) ?. toLowerCase ( ) === "true" ;
173+ const emailSendingEnabled =
174+ env ( "NEXT_PUBLIC_DISABLE_EMAIL" ) ?. toLowerCase ( ) !== "true" ;
175+ setIsEmailSendingEnabled ( emailSendingEnabled ) ;
171176 setIsCredentialsEnabled ( credentialsAllowed ) ;
172177 } , [ ] ) ;
173178
@@ -187,7 +192,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
187192
188193 const handleLoginWithEmail = async (
189194 email : string ,
190- password ?: string ,
195+ password ?: string | null ,
191196 name ?: string ,
192197 ) => {
193198 setIsLoginWithEmailPending ( true ) ;
@@ -230,16 +235,26 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
230235 ) ;
231236 }
232237 } else {
233- await authClient . signIn . magicLink (
234- {
235- email,
236- callbackURL,
237- } ,
238- {
239- onSuccess : ( ) => setIsMagicLinkSent ( true , email ) ,
240- onError : ( { error } ) => setLoginError ( error . message ) ,
241- } ,
242- ) ;
238+ // Only allow magic link if email sending is enabled and not in sign up mode
239+ if ( isEmailSendingEnabled && ! isSignUp ) {
240+ await authClient . signIn . magicLink (
241+ {
242+ email,
243+ callbackURL,
244+ } ,
245+ {
246+ onSuccess : ( ) => setIsMagicLinkSent ( true , email ) ,
247+ onError : ( { error } ) => setLoginError ( error . message ) ,
248+ } ,
249+ ) ;
250+ } else {
251+ // Provide a clear error feedback when password omitted but magic link unavailable
252+ setLoginError (
253+ isSignUp
254+ ? t `Password is required to sign up.`
255+ : t `Password is required to login.` ,
256+ ) ;
257+ }
243258 }
244259
245260 setIsLoginWithEmailPending ( false ) ;
@@ -276,11 +291,43 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
276291 } ;
277292
278293 const onSubmit = async ( values : FormValues ) => {
279- await handleLoginWithEmail ( values . email , values . password , values . name ) ;
294+ // Treat empty password string as undefined to trigger magic link path
295+ const sanitizedPassword = values . password ?. trim ( )
296+ ? values . password
297+ : undefined ;
298+ await handleLoginWithEmail ( values . email , sanitizedPassword , values . name ) ;
280299 } ;
281300
282301 const password = watch ( "password" ) ;
283302
303+ // Determine if we should operate in magic link mode for current form state (login only)
304+ const isMagicLinkMode = useMemo ( ( ) => {
305+ // Magic link only viable when email sending enabled AND not sign up.
306+ if ( ! isEmailSendingEnabled || isSignUp ) return false ;
307+ // If credentials disabled we always default to magic link.
308+ if ( ! isCredentialsEnabled ) return true ;
309+ // Credentials enabled: user chooses magic link by leaving password blank.
310+ return ! password ;
311+ } , [ isEmailSendingEnabled , isSignUp , isCredentialsEnabled , password ] ) ;
312+
313+ // Auto-focus password field when an error indicates it's required
314+ useEffect ( ( ) => {
315+ if ( ! isCredentialsEnabled ) return ;
316+ // Focus when: sign up and missing password; login error requiring password; validation error on password.
317+ const pwdEmpty = ( password ?? "" ) . length === 0 ;
318+ let needsPassword = false ;
319+ if ( isSignUp && pwdEmpty ) {
320+ needsPassword = true ;
321+ } else if ( loginError ?. toLowerCase ( ) . includes ( "password" ) ) {
322+ needsPassword = true ;
323+ } else if ( errors . password ) {
324+ needsPassword = true ;
325+ }
326+ if ( needsPassword && passwordRef . current ) {
327+ passwordRef . current . focus ( ) ;
328+ }
329+ } , [ isSignUp , password , loginError , errors . password , isCredentialsEnabled ] ) ;
330+
284331 return (
285332 < div className = "space-y-6" >
286333 { socialProviders ?. length !== 0 && (
@@ -351,7 +398,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
351398 />
352399 { errors . password && (
353400 < p className = "mt-2 text-xs text-red-400" >
354- { t `Please enter a valid password` }
401+ { errors . password . message ?? t `Please enter a valid password` }
355402 </ p >
356403 ) }
357404 </ div >
@@ -368,9 +415,7 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
368415 variant = "secondary"
369416 >
370417 { isSignUp ? t `Sign up with ` : t `Continue with ` }
371- { ! isCredentialsEnabled || ( password && password . length !== 0 )
372- ? t `email`
373- : t `magic link` }
418+ { isMagicLinkMode ? t `magic link` : t `email` }
374419 </ Button >
375420 </ div >
376421 </ form >
0 commit comments