diff --git a/frontend/app/components/Sidebar.tsx b/frontend/app/components/Sidebar.tsx index 2c0c015..6de19c7 100644 --- a/frontend/app/components/Sidebar.tsx +++ b/frontend/app/components/Sidebar.tsx @@ -18,7 +18,9 @@ export default function Sidebar() { return location.pathname.startsWith(path); }; - const navItems: Array<{ label: string; path: string; icon: typeof Home; disabled?: boolean }> = [ + type NavItem = { label: string; path: string; icon: typeof Home; disabled?: boolean }; + + const explorerItems: NavItem[] = [ { label: 'Home Page', path: '/', icon: Home }, { label: 'Analytics', path: '/analytics', icon: BarChart3 }, { label: 'Transactions', path: '/txs', icon: ArrowRightLeft }, @@ -30,8 +32,11 @@ export default function Sidebar() { { label: 'Scheduled Txs', path: '/scheduled', icon: Clock }, { label: 'Nodes', path: '/nodes', icon: Globe }, { label: 'Indexing Status', path: '/stats', icon: Layers }, + ]; + + const developerItems: NavItem[] = [ { label: 'API Docs', path: '/api-docs', icon: FileText }, - { label: 'Developer', path: '/developer', icon: Code2 }, + { label: 'Developer Portal', path: '/developer', icon: Code2 }, ]; return ( @@ -64,7 +69,7 @@ export default function Sidebar() { {/* Nav Items */} {/* Footer */} @@ -125,12 +152,12 @@ export default function Sidebar() { {/* Navigation */} {/* Footer / Controls */} diff --git a/frontend/app/contexts/AuthContext.tsx b/frontend/app/contexts/AuthContext.tsx index 64c6593..be24ea8 100644 --- a/frontend/app/contexts/AuthContext.tsx +++ b/frontend/app/contexts/AuthContext.tsx @@ -20,6 +20,7 @@ interface AuthContextValue extends AuthState { signUp: (email: string, password: string) => Promise; signIn: (email: string, password: string) => Promise; sendMagicLink: (email: string) => Promise; + verifyOtp: (email: string, token: string) => Promise; handleCallback: (hash: string) => void; signOut: () => void; } @@ -231,6 +232,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { await gotruePost('/magiclink', { email }); }, []); + const verifyOtp = useCallback( + async (email: string, token: string) => { + const data = await gotruePost('/verify', { type: 'magiclink', token, redirect_to: '', email }); + applyTokenResponse(data); + }, + [applyTokenResponse], + ); + const handleCallback = useCallback( (hash: string) => { const params = new URLSearchParams(hash.replace(/^#/, '')); @@ -257,6 +266,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { signUp, signIn, sendMagicLink, + verifyOtp, handleCallback, signOut, }} diff --git a/frontend/app/routes/developer/login.tsx b/frontend/app/routes/developer/login.tsx index a2d2ed6..26c1635 100644 --- a/frontend/app/routes/developer/login.tsx +++ b/frontend/app/routes/developer/login.tsx @@ -1,25 +1,24 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { motion } from 'framer-motion' -import { Mail, Lock, ArrowRight, Loader2, Sparkles, UserPlus, LogIn } from 'lucide-react' +import { Mail, ArrowRight, Loader2, Sparkles, Wallet } from 'lucide-react' import { useAuth } from '../../contexts/AuthContext' export const Route = createFileRoute('/developer/login')({ component: DeveloperLoginPage, }) -type AuthMode = 'login' | 'register' | 'magic' - function DeveloperLoginPage() { - const { user, loading: authLoading, signIn, signUp, sendMagicLink } = useAuth() + const { user, loading: authLoading, sendMagicLink, verifyOtp } = useAuth() const navigate = useNavigate() - const [mode, setMode] = useState('login') const [email, setEmail] = useState('') - const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - const [magicLinkSent, setMagicLinkSent] = useState(false) + const [otpSent, setOtpSent] = useState(false) + const [otp, setOtp] = useState(['', '', '', '', '', '']) + const [verifying, setVerifying] = useState(false) + const otpRefs = useRef<(HTMLInputElement | null)[]>([]) // Redirect if already logged in useEffect(() => { @@ -28,34 +27,73 @@ function DeveloperLoginPage() { } }, [authLoading, user, navigate]) - async function handleSubmit(e: React.FormEvent) { + async function handleSendLink(e: React.FormEvent) { e.preventDefault() setError(null) setLoading(true) try { - if (mode === 'magic') { - await sendMagicLink(email) - setMagicLinkSent(true) - } else if (mode === 'register') { - await signUp(email, password) - // After signup, try to sign in (if auto-confirmed) - try { - await signIn(email, password) - } catch { - // If sign-in fails, user may need to confirm email - setError('Account created. Please check your email to confirm, then sign in.') - } - } else { - await signIn(email, password) - } + await sendMagicLink(email) + setOtpSent(true) } catch (err) { - setError(err instanceof Error ? err.message : 'Authentication failed') + setError(err instanceof Error ? err.message : 'Failed to send magic link') } finally { setLoading(false) } } + function handleOtpChange(index: number, value: string) { + if (value.length > 1) { + // Handle paste of full OTP + const digits = value.replace(/\D/g, '').slice(0, 6).split('') + const newOtp = [...otp] + digits.forEach((d, i) => { + if (index + i < 6) newOtp[index + i] = d + }) + setOtp(newOtp) + const nextIndex = Math.min(index + digits.length, 5) + otpRefs.current[nextIndex]?.focus() + // Auto-submit if all filled + if (newOtp.every(d => d !== '')) { + submitOtp(newOtp.join('')) + } + return + } + + const newOtp = [...otp] + newOtp[index] = value.replace(/\D/g, '') + setOtp(newOtp) + + if (value && index < 5) { + otpRefs.current[index + 1]?.focus() + } + + // Auto-submit when all 6 digits entered + if (value && newOtp.every(d => d !== '')) { + submitOtp(newOtp.join('')) + } + } + + function handleOtpKeyDown(index: number, e: React.KeyboardEvent) { + if (e.key === 'Backspace' && !otp[index] && index > 0) { + otpRefs.current[index - 1]?.focus() + } + } + + async function submitOtp(code: string) { + setError(null) + setVerifying(true) + try { + await verifyOtp(email, code) + } catch (err) { + setError(err instanceof Error ? err.message : 'Invalid code. Please try again.') + setOtp(['', '', '', '', '', '']) + otpRefs.current[0]?.focus() + } finally { + setVerifying(false) + } + } + if (authLoading) { return (
@@ -64,8 +102,8 @@ function DeveloperLoginPage() { ) } - // Magic link sent confirmation - if (magicLinkSent) { + // OTP verification screen + if (otpSent) { return (

Check your email

-

- We sent a magic link to {email}. - Click the link in the email to sign in. +

+ We sent a sign-in link and code to

- +

{email}

+ + {/* OTP Input */} +

Enter the 6-digit code:

+ + {error && ( + + {error} + + )} + +
+ {otp.map((digit, i) => ( + { otpRefs.current[i] = el }} + type="text" + inputMode="numeric" + maxLength={6} + value={digit} + onChange={e => handleOtpChange(i, e.target.value)} + onKeyDown={e => handleOtpKeyDown(i, e)} + disabled={verifying} + className="w-11 h-13 text-center text-xl font-bold bg-neutral-800 border border-neutral-700 rounded-lg text-[#00ef8b] focus:outline-none focus:border-[#00ef8b]/50 focus:ring-1 focus:ring-[#00ef8b]/20 transition-colors disabled:opacity-50" + autoFocus={i === 0} + /> + ))} +
+ + {verifying && ( +
+ + Verifying... +
+ )} + +
+

Or click the magic link in your email

+
+ + | + +
+
) } - const titles: Record = { - login: 'Sign in', - register: 'Create account', - magic: 'Magic link', - } - - const descriptions: Record = { - login: 'Sign in to your developer portal', - register: 'Create a new developer account', - magic: 'Sign in with a link sent to your email', - } - return (
-

{titles[mode]}

-

{descriptions[mode]}

+

Developer Portal

+

Sign in to manage your webhooks and API keys

{/* Error */} @@ -135,8 +224,7 @@ function DeveloperLoginPage() { )} {/* Form */} -
- {/* Email */} +
- {/* Password (hidden in magic link mode) */} - {mode !== 'magic' && ( - - -
- - setPassword(e.target.value)} - placeholder="Enter your password" - className="w-full pl-10 pr-4 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder:text-neutral-500 focus:outline-none focus:border-[#00ef8b]/50 focus:ring-1 focus:ring-[#00ef8b]/20 transition-colors text-sm" - /> -
-
- )} - - {/* Submit */}
- {/* Mode toggles */} -
- {mode === 'login' && ( - <> -

- Don't have an account?{' '} - -

-

- or{' '} - -

- - )} - {mode === 'register' && ( -

- Already have an account?{' '} - -

- )} - {mode === 'magic' && ( -

- -

- )} + {/* Divider */} +
+
+ or +
+ + {/* Wallet login - coming soon */} + + +

+ We'll send you a magic link and verification code +