@@ -7,6 +7,7 @@ import { useForm } from 'react-hook-form';
77import { toast } from 'sonner' ;
88import z from 'zod' ;
99import LoginForm from './LoginForm' ;
10+ import TwoFactorVerify from './TwoFactorVerify' ;
1011import { authClient } from '@/lib/auth-client' ;
1112import { useAuthStore } from '@/lib/stores/auth-store' ;
1213
@@ -32,6 +33,7 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
3233 const [ showPassword , setShowPassword ] = useState ( false ) ;
3334 const [ isLoading , setIsLoading ] = useState ( false ) ;
3435 const [ lastMethod , setLastMethod ] = useState < string | null > ( null ) ;
36+ const [ twoFactorRequired , setTwoFactorRequired ] = useState ( false ) ;
3537
3638 const callbackUrl = searchParams . get ( 'callbackUrl' )
3739 ? decodeURIComponent ( searchParams . get ( 'callbackUrl' ) ! )
@@ -42,16 +44,6 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
4244 setLastMethod ( method ) ;
4345 } , [ ] ) ;
4446
45- useEffect ( ( ) => {
46- const method = authClient . getLastUsedLoginMethod ( ) ;
47- setLastMethod ( method ) ;
48- } , [ ] ) ;
49-
50- useEffect ( ( ) => {
51- const method = authClient . getLastUsedLoginMethod ( ) ;
52- setLastMethod ( method ) ;
53- } , [ ] ) ;
54-
5547 const form = useForm < FormData > ( {
5648 resolver : zodResolver ( formSchema ) ,
5749 defaultValues : {
@@ -89,6 +81,17 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
8981 const errorStatus = error . status ;
9082 const errorCode = error . code ;
9183
84+ // Check for 2FA requirement
85+ if (
86+ errorStatus === 403 &&
87+ ( errorMessage === 'two_factor_required' ||
88+ errorMessage ?. includes ( 'two_factor' ) ||
89+ errorCode === 'TWO_FACTOR_REQUIRED' )
90+ ) {
91+ setTwoFactorRequired ( true ) ;
92+ return ;
93+ }
94+
9295 if ( errorStatus === 403 || errorCode === 'FORBIDDEN' ) {
9396 const message =
9497 'Please verify your email before signing in. Check your inbox for a verification link.' ;
@@ -171,8 +174,36 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
171174 setIsLoading ( true ) ;
172175 setLoadingState ( true ) ;
173176
177+ const syncSession = async ( ) => {
178+ const session = await authClient . getSession ( ) ;
179+ if ( session && typeof session === 'object' && 'user' in session ) {
180+ const sessionUser = session . user as
181+ | {
182+ id : string ;
183+ email : string ;
184+ name ?: string | null ;
185+ image ?: string | null ;
186+ }
187+ | null
188+ | undefined ;
189+
190+ if ( sessionUser && sessionUser . id && sessionUser . email ) {
191+ const authStore = useAuthStore . getState ( ) ;
192+ await authStore . syncWithSession ( {
193+ id : sessionUser . id ,
194+ email : sessionUser . email ,
195+ name : sessionUser . name || undefined ,
196+ image : sessionUser . image || undefined ,
197+ role : 'USER' ,
198+ username : undefined ,
199+ accessToken : undefined ,
200+ } ) ;
201+ }
202+ }
203+ } ;
204+
174205 try {
175- const { error } = await authClient . signIn . email (
206+ const { data , error } = await authClient . signIn . email (
176207 {
177208 email : values . email ,
178209 password : values . password ,
@@ -184,42 +215,26 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
184215 setIsLoading ( true ) ;
185216 setLoadingState ( true ) ;
186217 } ,
187- onSuccess : async ( ) => {
188- await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
218+ onSuccess : async ctx => {
219+ const resData = ctx ?. data as
220+ | {
221+ twoFactorRequired ?: boolean ;
222+ twoFactorRedirect ?: boolean ;
223+ }
224+ | undefined ;
189225
190- const session = await authClient . getSession ( ) ;
191-
192- if ( session && typeof session === 'object' && 'user' in session ) {
193- const sessionUser = session . user as
194- | {
195- id : string ;
196- email : string ;
197- name ?: string | null ;
198- image ?: string | null ;
199- }
200- | null
201- | undefined ;
202-
203- if ( sessionUser && sessionUser . id && sessionUser . email ) {
204- const authStore = useAuthStore . getState ( ) ;
205- await authStore . syncWithSession ( {
206- id : sessionUser . id ,
207- email : sessionUser . email ,
208- name : sessionUser . name || undefined ,
209- image : sessionUser . image || undefined ,
210- role : 'USER' ,
211- username : undefined ,
212- accessToken : undefined ,
213- } ) ;
214- }
226+ if ( resData ?. twoFactorRequired || resData ?. twoFactorRedirect ) {
227+ setTwoFactorRequired ( true ) ;
228+ setIsLoading ( false ) ;
229+ setLoadingState ( false ) ;
230+ return ;
215231 }
216232
217- // Keep loading state active during redirect
218- // The page will unmount when redirecting, so no need to set false
233+ await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
234+ await syncSession ( ) ;
219235 window . location . href = callbackUrl ;
220236 } ,
221237 onError : ctx => {
222- // Handle error from Better Auth callback
223238 const errorObj = ctx . error || ctx ;
224239 handleAuthError (
225240 typeof errorObj === 'object'
@@ -233,14 +248,21 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
233248 }
234249 ) ;
235250
236- // Handle error from return value
237251 if ( error ) {
238252 handleAuthError ( error , values ) ;
239253 setIsLoading ( false ) ;
240254 setLoadingState ( false ) ;
255+ } else if (
256+ ( data as { twoFactorRequired ?: boolean ; twoFactorRedirect ?: boolean } )
257+ ?. twoFactorRequired ||
258+ ( data as { twoFactorRequired ?: boolean ; twoFactorRedirect ?: boolean } )
259+ ?. twoFactorRedirect
260+ ) {
261+ setTwoFactorRequired ( true ) ;
262+ setIsLoading ( false ) ;
263+ setLoadingState ( false ) ;
241264 }
242265 } catch ( error ) {
243- // Handle unexpected errors
244266 const errorObj =
245267 error instanceof Error
246268 ? { message : error . message , status : undefined , code : undefined }
@@ -254,6 +276,17 @@ const LoginWrapper = ({ setLoadingState }: LoginWrapperProps) => {
254276 [ handleAuthError , setLoadingState , callbackUrl ]
255277 ) ;
256278
279+ if ( twoFactorRequired ) {
280+ return (
281+ < TwoFactorVerify
282+ onSuccess = { async ( ) => {
283+ window . location . href = callbackUrl ;
284+ } }
285+ onCancel = { ( ) => setTwoFactorRequired ( false ) }
286+ />
287+ ) ;
288+ }
289+
257290 return (
258291 < LoginForm
259292 form = { form }
0 commit comments