11"use client" ;
22
3- import React , { useState } from "react" ;
3+ import React , { useState , useEffect } from "react" ;
44import { useAuth } from "../contexts/AuthContext" ;
55import { Button } from "./Button" ;
66
@@ -15,10 +15,34 @@ export const AuthModal: React.FC<AuthModalProps> = ({
1515 onClose,
1616 trialExpired = false ,
1717} ) => {
18- const { signInWithEmail, signInWithGoogle, signInWithApple } = useAuth ( ) ;
18+ const { signInWithEmail, verifyEmailOTP, signInWithGoogle, signInWithApple } =
19+ useAuth ( ) ;
1920 const [ email , setEmail ] = useState ( "" ) ;
21+ const [ otp , setOtp ] = useState ( "" ) ;
22+ const [ userId , setUserId ] = useState < string | null > ( null ) ;
2023 const [ isLoading , setIsLoading ] = useState ( false ) ;
24+ const [ isVerifying , setIsVerifying ] = useState ( false ) ;
25+ const [ isRedirecting , setIsRedirecting ] = useState ( false ) ;
2126 const [ message , setMessage ] = useState ( "" ) ;
27+ const [ step , setStep ] = useState < "email" | "otp" > ( "email" ) ;
28+ const [ lastUsedMethod , setLastUsedMethod ] = useState < {
29+ type : string ;
30+ value : string ;
31+ } | null > ( null ) ;
32+
33+ // Load last used method from localStorage
34+ useEffect ( ( ) => {
35+ if ( isOpen ) {
36+ const saved = localStorage . getItem ( "lastUsedAuthMethod" ) ;
37+ if ( saved ) {
38+ try {
39+ setLastUsedMethod ( JSON . parse ( saved ) ) ;
40+ } catch ( e ) {
41+ // Invalid JSON, ignore
42+ }
43+ }
44+ }
45+ } , [ isOpen ] ) ;
2246
2347 if ( ! isOpen ) return null ;
2448
@@ -31,11 +55,16 @@ export const AuthModal: React.FC<AuthModalProps> = ({
3155
3256 try {
3357 const result = await signInWithEmail ( email . trim ( ) ) ;
34- if ( result . success ) {
35- setMessage ( "Check your email for the login link!" ) ;
36- setEmail ( "" ) ;
58+ if ( result . success && result . userId ) {
59+ setUserId ( result . userId ) ;
60+ // Save last used method
61+ const method = { type : "email" , value : email . trim ( ) } ;
62+ setLastUsedMethod ( method ) ;
63+ localStorage . setItem ( "lastUsedAuthMethod" , JSON . stringify ( method ) ) ;
64+ setStep ( "otp" ) ;
65+ setMessage ( "" ) ;
3766 } else {
38- setMessage ( result . error || "Failed to send login link " ) ;
67+ setMessage ( result . error || "Failed to send OTP " ) ;
3968 }
4069 } catch ( error ) {
4170 setMessage ( "An error occurred. Please try again." ) ;
@@ -44,9 +73,44 @@ export const AuthModal: React.FC<AuthModalProps> = ({
4473 }
4574 } ;
4675
76+ const handleOTPVerification = async ( e : React . FormEvent ) => {
77+ e . preventDefault ( ) ;
78+ if ( ! otp . trim ( ) || ! userId ) return ;
79+
80+ setIsVerifying ( true ) ;
81+ setMessage ( "" ) ;
82+
83+ try {
84+ const result = await verifyEmailOTP ( userId , otp . trim ( ) ) ;
85+ if ( result . success ) {
86+ setIsVerifying ( false ) ;
87+ setIsRedirecting ( true ) ;
88+ setMessage ( "Redirecting..." ) ;
89+ setTimeout ( ( ) => {
90+ onClose ( ) ;
91+ setStep ( "email" ) ;
92+ setEmail ( "" ) ;
93+ setOtp ( "" ) ;
94+ setUserId ( null ) ;
95+ setIsRedirecting ( false ) ;
96+ } , 2000 ) ;
97+ } else {
98+ setIsVerifying ( false ) ;
99+ setMessage ( result . error || "Invalid OTP code" ) ;
100+ }
101+ } catch ( error ) {
102+ setIsVerifying ( false ) ;
103+ setMessage ( "An error occurred. Please try again." ) ;
104+ }
105+ } ;
106+
47107 const handleGoogleSignIn = async ( ) => {
48108 setIsLoading ( true ) ;
49109 try {
110+ // Save last used method
111+ const method = { type : "google" , value : "Google" } ;
112+ setLastUsedMethod ( method ) ;
113+ localStorage . setItem ( "lastUsedAuthMethod" , JSON . stringify ( method ) ) ;
50114 await signInWithGoogle ( ) ;
51115 } catch ( error ) {
52116 setMessage ( "Failed to sign in with Google" ) ;
@@ -57,6 +121,10 @@ export const AuthModal: React.FC<AuthModalProps> = ({
57121 const handleAppleSignIn = async ( ) => {
58122 setIsLoading ( true ) ;
59123 try {
124+ // Save last used method
125+ const method = { type : "apple" , value : "Apple" } ;
126+ setLastUsedMethod ( method ) ;
127+ localStorage . setItem ( "lastUsedAuthMethod" , JSON . stringify ( method ) ) ;
60128 await signInWithApple ( ) ;
61129 } catch ( error ) {
62130 setMessage ( "Failed to sign in with Apple" ) ;
@@ -89,34 +157,169 @@ export const AuthModal: React.FC<AuthModalProps> = ({
89157 </ div >
90158
91159 { /* Email OTP Sign In */ }
92- < form onSubmit = { handleEmailSignIn } className = "mb-6" >
93- < div className = "mb-4" >
94- < label
95- htmlFor = "email"
96- className = "block text-sm font-medium text-slate-300 mb-2"
160+ { step === "email" ? (
161+ < form onSubmit = { handleEmailSignIn } className = "mb-6" >
162+ < div className = "mb-4" >
163+ < label
164+ htmlFor = "email"
165+ className = "block text-sm font-medium text-slate-300 mb-2"
166+ >
167+ Email Address
168+ </ label >
169+ < input
170+ type = "email"
171+ id = "email"
172+ value = { email }
173+ onChange = { ( e ) => setEmail ( e . target . value ) }
174+ placeholder = "Enter your email"
175+ className = "w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
176+ required
177+ />
178+ </ div >
179+ < Button
180+ type = "submit"
181+ intent = "primary"
182+ size = "medium"
183+ disabled = { isLoading }
184+ className = "w-full relative flex items-center justify-center"
97185 >
98- Email Address
99- </ label >
100- < input
101- type = "email"
102- id = "email"
103- value = { email }
104- onChange = { ( e ) => setEmail ( e . target . value ) }
105- placeholder = "Enter your email"
106- className = "w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
107- required
108- />
186+ { isLoading && (
187+ < svg
188+ className = "animate-spin mr-2 h-4 w-4 text-white"
189+ xmlns = "http://www.w3.org/2000/svg"
190+ fill = "none"
191+ viewBox = "0 0 24 24"
192+ >
193+ < circle
194+ className = "opacity-25"
195+ cx = "12"
196+ cy = "12"
197+ r = "10"
198+ stroke = "currentColor"
199+ strokeWidth = "4"
200+ > </ circle >
201+ < path
202+ className = "opacity-75"
203+ fill = "currentColor"
204+ d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
205+ > </ path >
206+ </ svg >
207+ ) }
208+ { isLoading ? "Sending..." : "Continue with Email" }
209+ { ! isLoading && lastUsedMethod ?. type === "email" && (
210+ < span className = "absolute -top-1 -right-1 bg-blue-500 text-white text-xs px-2 py-1 rounded-full" >
211+ Last Used
212+ </ span >
213+ ) }
214+ </ Button >
215+ </ form >
216+ ) : (
217+ < div className = "mb-6" >
218+ < div className = "text-center mb-6" >
219+ < h3 className = "text-xl font-semibold text-white mb-2" >
220+ Verification
221+ </ h3 >
222+ < p className = "text-slate-300 text-sm" >
223+ If you have an account, we have sent a code to{ " " }
224+ < span className = "font-medium text-white" >
225+ { lastUsedMethod ?. value }
226+ </ span >
227+ .
228+ < br />
229+ Enter it below.
230+ </ p >
231+ </ div >
232+
233+ < form onSubmit = { handleOTPVerification } className = "mb-4" >
234+ < div className = "mb-4" >
235+ < div className = "flex gap-2 justify-center" >
236+ { [ 0 , 1 , 2 , 3 , 4 , 5 ] . map ( ( index ) => (
237+ < input
238+ key = { index }
239+ type = "text"
240+ value = { otp [ index ] || "" }
241+ onChange = { ( e ) => {
242+ const newOtp = otp . split ( "" ) ;
243+ newOtp [ index ] = e . target . value
244+ . replace ( / \D / g, "" )
245+ . slice ( 0 , 1 ) ;
246+ setOtp ( newOtp . join ( "" ) ) ;
247+ // Auto-focus next input
248+ if ( e . target . value && index < 5 ) {
249+ const nextInput = document . getElementById (
250+ `otp-${ index + 1 } ` ,
251+ ) ;
252+ nextInput ?. focus ( ) ;
253+ }
254+ } }
255+ className = "w-12 h-12 text-center text-lg font-mono bg-slate-700 border border-slate-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
256+ maxLength = { 1 }
257+ id = { `otp-${ index } ` }
258+ required
259+ />
260+ ) ) }
261+ </ div >
262+ </ div >
263+
264+ { ( isVerifying || isRedirecting ) && (
265+ < div className = "text-center mb-4" >
266+ < div className = "flex items-center justify-center gap-2 text-white" >
267+ < span > { isVerifying ? "Verifying" : "Redirecting" } </ span >
268+ < svg
269+ className = "animate-spin h-4 w-4"
270+ xmlns = "http://www.w3.org/2000/svg"
271+ fill = "none"
272+ viewBox = "0 0 24 24"
273+ >
274+ < circle
275+ className = "opacity-25"
276+ cx = "12"
277+ cy = "12"
278+ r = "10"
279+ stroke = "currentColor"
280+ strokeWidth = "4"
281+ > </ circle >
282+ < path
283+ className = "opacity-75"
284+ fill = "currentColor"
285+ d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
286+ > </ path >
287+ </ svg >
288+ </ div >
289+ </ div >
290+ ) }
291+
292+ < Button
293+ type = "submit"
294+ intent = "primary"
295+ size = "medium"
296+ disabled = { isVerifying || isRedirecting || otp . length !== 6 }
297+ className = "w-full"
298+ >
299+ { isVerifying
300+ ? "Verifying..."
301+ : isRedirecting
302+ ? "Redirecting..."
303+ : "Verify Code" }
304+ </ Button >
305+ </ form >
306+
307+ < button
308+ type = "button"
309+ onClick = { ( ) => {
310+ setStep ( "email" ) ;
311+ setOtp ( "" ) ;
312+ setUserId ( null ) ;
313+ setMessage ( "" ) ;
314+ setIsVerifying ( false ) ;
315+ setIsRedirecting ( false ) ;
316+ } }
317+ className = "w-full text-center text-sm text-blue-400 hover:text-blue-300"
318+ >
319+ ← Back
320+ </ button >
109321 </ div >
110- < Button
111- type = "submit"
112- intent = "primary"
113- size = "medium"
114- disabled = { isLoading }
115- className = "w-full"
116- >
117- { isLoading ? "Sending..." : "Send Login Link" }
118- </ Button >
119- </ form >
322+ ) }
120323
121324 { /* Divider */ }
122325 < div className = "flex items-center mb-6" >
@@ -133,7 +336,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({
133336 size = "medium"
134337 onClick = { handleGoogleSignIn }
135338 disabled = { isLoading }
136- className = "w-full flex items-center justify-center gap-3"
339+ className = "w-full flex items-center justify-center gap-3 relative "
137340 >
138341 < svg className = "w-5 h-5" viewBox = "0 0 24 24" >
139342 < path
@@ -154,6 +357,11 @@ export const AuthModal: React.FC<AuthModalProps> = ({
154357 />
155358 </ svg >
156359 Continue with Google
360+ { ! isLoading && lastUsedMethod ?. type === "google" && (
361+ < span className = "absolute -top-1 -right-1 bg-blue-500 text-white text-xs px-2 py-1 rounded-full" >
362+ Last Used
363+ </ span >
364+ ) }
157365 </ Button >
158366
159367 < Button
@@ -162,7 +370,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({
162370 size = "medium"
163371 onClick = { handleAppleSignIn }
164372 disabled = { isLoading }
165- className = "w-full flex items-center justify-center gap-3"
373+ className = "w-full flex items-center justify-center gap-3 relative "
166374 >
167375 < svg className = "w-5 h-5" viewBox = "0 0 24 24" >
168376 < path
@@ -171,6 +379,11 @@ export const AuthModal: React.FC<AuthModalProps> = ({
171379 />
172380 </ svg >
173381 Continue with Apple
382+ { ! isLoading && lastUsedMethod ?. type === "apple" && (
383+ < span className = "absolute -top-1 -right-1 bg-blue-500 text-white text-xs px-2 py-1 rounded-full" >
384+ Last Used
385+ </ span >
386+ ) }
174387 </ Button >
175388 </ div >
176389
0 commit comments