/** * User Authentication Components for Goose Ultra * * Components: * - LoginGate: Full-screen wrapper that enforces authentication * - UserOnboarding: Name + secret question wizard * - SecretCodeReveal: Shows code once with copy button * - LogoutDialog: Confirmation with clean data option */ import React, { useState, useEffect, createContext, useContext, ReactNode } from 'react'; // ===== TYPES ===== interface GooseUser { userId: string; displayName: string; secretQuestionId: string; createdAt: number; lastLoginAt: number; } interface UserSession { userId: string; displayName: string; loginAt: number; } interface SecretQuestion { id: string; question: string; } interface UserContextType { session: UserSession | null; user: GooseUser | null; isLoading: boolean; logout: (cleanData?: boolean) => Promise; refreshSession: () => Promise; } // ===== CONTEXT ===== const UserContext = createContext({ session: null, user: null, isLoading: true, logout: async () => { }, refreshSession: async () => { } }); export const useUser = () => useContext(UserContext); // ===== ICONS ===== const Icons = { User: () => ( ), Key: () => ( ), Check: () => ( ), Copy: () => ( ), Alert: () => ( ), Logout: () => ( ), Trash: () => ( ), ArrowRight: () => ( ) }; // ===== STYLES ===== const styles = { container: { position: 'fixed' as const, inset: 0, background: 'linear-gradient(135deg, #030304 0%, #0a0a0f 50%, #030304 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif" }, card: { background: 'rgba(20, 20, 25, 0.95)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '24px', padding: '48px', maxWidth: '480px', width: '100%', boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.8)', backdropFilter: 'blur(20px)' }, logo: { width: '80px', height: '80px', margin: '0 auto 24px', display: 'block', borderRadius: '20px', background: 'linear-gradient(135deg, #22d3ee 0%, #3b82f6 100%)', padding: '16px' }, title: { fontSize: '28px', fontWeight: 700, textAlign: 'center' as const, marginBottom: '8px', background: 'linear-gradient(135deg, #fff 0%, #a1a1aa 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }, subtitle: { fontSize: '15px', color: '#71717a', textAlign: 'center' as const, marginBottom: '32px' }, input: { width: '100%', padding: '14px 16px', background: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '12px', color: '#fff', fontSize: '16px', outline: 'none', transition: 'all 0.2s', marginBottom: '16px', boxSizing: 'border-box' as const }, select: { width: '100%', padding: '14px 16px', background: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '12px', color: '#fff', fontSize: '16px', outline: 'none', marginBottom: '16px', cursor: 'pointer', boxSizing: 'border-box' as const }, button: { width: '100%', padding: '16px', background: 'linear-gradient(135deg, #22d3ee 0%, #3b82f6 100%)', border: 'none', borderRadius: '12px', color: '#000', fontSize: '16px', fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', transition: 'all 0.2s' }, buttonSecondary: { width: '100%', padding: '16px', background: 'rgba(255, 255, 255, 0.05)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '12px', color: '#fff', fontSize: '16px', fontWeight: 500, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '8px', transition: 'all 0.2s', marginTop: '12px' }, codeBox: { background: 'rgba(0, 0, 0, 0.6)', border: '2px solid rgba(34, 211, 238, 0.3)', borderRadius: '16px', padding: '24px', textAlign: 'center' as const, marginBottom: '24px' }, code: { fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: '28px', fontWeight: 700, color: '#22d3ee', letterSpacing: '2px' }, warning: { background: 'rgba(234, 179, 8, 0.1)', border: '1px solid rgba(234, 179, 8, 0.3)', borderRadius: '12px', padding: '16px', display: 'flex', gap: '12px', alignItems: 'flex-start', marginBottom: '24px', color: '#eab308' }, checkbox: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '24px', cursor: 'pointer', color: '#a1a1aa' }, error: { background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)', borderRadius: '12px', padding: '12px 16px', color: '#ef4444', fontSize: '14px', marginBottom: '16px', textAlign: 'center' as const } }; // ===== COMPONENTS ===== /** * Welcome Screen - First screen user sees */ const WelcomeScreen: React.FC<{ onNewUser: () => void; onHasCode: () => void; }> = ({ onNewUser, onHasCode }) => (

Welcome to Goose Ultra

Your personal AI-powered development environment

); /** * Login Screen - Enter secret code */ const LoginScreen: React.FC<{ onLogin: (code: string) => Promise; onBack: () => void; }> = ({ onLogin, onBack }) => { const [code, setCode] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!code.trim()) return; setLoading(true); setError(''); const success = await onLogin(code.trim()); if (!success) { setError('Invalid secret code. Please check and try again.'); } setLoading(false); }; return (

Welcome Back

Enter your secret code to continue

{error &&
{error}
}
setCode(e.target.value.toUpperCase())} style={{ ...styles.input, fontFamily: "'JetBrains Mono', monospace", letterSpacing: '1px', textAlign: 'center', fontSize: '18px' }} autoFocus />
); }; /** * Onboarding Step 1 - Enter Name */ const OnboardingName: React.FC<{ onNext: (name: string) => void; onBack: () => void; }> = ({ onNext, onBack }) => { const [name, setName] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (name.trim().length >= 2) { onNext(name.trim()); } }; return (

What's your name?

This will be displayed in your profile

setName(e.target.value)} style={styles.input} autoFocus minLength={2} maxLength={50} />
); }; /** * Onboarding Step 2 - Secret Question */ const OnboardingQuestion: React.FC<{ questions: SecretQuestion[]; onNext: (questionId: string, answer: string) => void; onBack: () => void; }> = ({ questions, onNext, onBack }) => { const [questionId, setQuestionId] = useState(''); const [answer, setAnswer] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (questionId && answer.trim().length >= 2) { onNext(questionId, answer.trim()); } }; return (

Set a Security Question

This helps generate your unique secret code

setAnswer(e.target.value)} style={styles.input} minLength={2} />
); }; /** * Secret Code Reveal */ const SecretCodeReveal: React.FC<{ code: string; userName: string; onContinue: () => void; }> = ({ code, userName, onContinue }) => { const [copied, setCopied] = useState(false); const [confirmed, setConfirmed] = useState(false); const copyCode = async () => { try { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (e) { console.error('Failed to copy:', e); } }; return (
🎉

Welcome, {userName}!

Your secret code is ready

{code}
SAVE THIS CODE OFFLINE! This is the ONLY way to log back in. We cannot recover it if you lose it.
); }; /** * Logout Dialog */ export const LogoutDialog: React.FC<{ isOpen: boolean; onClose: () => void; onLogout: (cleanData: boolean) => void; userName: string; stats: { projectCount: number; chatCount: number; totalSizeBytes: number }; }> = ({ isOpen, onClose, onLogout, userName, stats }) => { if (!isOpen) return null; const formatSize = (bytes: number) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; return (

Logging Out

Goodbye, {userName}!

Your data on this device:
Projects {stats.projectCount}
Chat History {stats.chatCount}
Total Size {formatSize(stats.totalSizeBytes)}
); }; // ===== MAIN LOGIN GATE ===== type Screen = 'welcome' | 'login' | 'onboarding-name' | 'onboarding-question' | 'code-reveal'; export const LoginGate: React.FC<{ children: ReactNode }> = ({ children }) => { const [screen, setScreen] = useState('welcome'); const [session, setSession] = useState(null); const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [questions, setQuestions] = useState([]); // Onboarding state const [onboardingName, setOnboardingName] = useState(''); const [generatedCode, setGeneratedCode] = useState(''); // Check for existing session on mount useEffect(() => { const checkSession = async () => { const electron = (window as any).electron; if (!electron?.user) { // No electron API (web mode) - skip auth setIsLoading(false); return; } try { const existingSession = await electron.user.getSession(); if (existingSession) { setSession(existingSession); } const questionsList = await electron.user.getSecretQuestions(); setQuestions(questionsList || []); } catch (e) { console.error('Failed to check session:', e); } setIsLoading(false); }; checkSession(); }, []); // Login handler const handleLogin = async (code: string): Promise => { const electron = (window as any).electron; if (!electron?.user) return false; const result = await electron.user.login(code); if (result.success) { setSession(result.session); setUser(result.user); return true; } return false; }; // Create user handler const handleCreateUser = async (questionId: string, answer: string) => { const electron = (window as any).electron; if (!electron?.user) return; const result = await electron.user.create(onboardingName, questionId, answer); if (result.success) { setGeneratedCode(result.secretCode); setUser(result.user); setSession(result.session); setScreen('code-reveal'); } }; // Logout handler const handleLogout = async (cleanData = false) => { const electron = (window as any).electron; if (!electron?.user) return; await electron.user.logout(cleanData); setSession(null); setUser(null); setScreen('welcome'); }; // Refresh session const refreshSession = async () => { const electron = (window as any).electron; if (!electron?.user) return; const existingSession = await electron.user.getSession(); setSession(existingSession); }; // Loading state if (isLoading) { return (
Loading...
); } // If no electron (web mode) or has session, render children const electron = (window as any).electron; if (!electron?.user || session) { return ( {children} ); } // Render login/onboarding screens return (
{screen === 'welcome' && ( setScreen('onboarding-name')} onHasCode={() => setScreen('login')} /> )} {screen === 'login' && ( setScreen('welcome')} /> )} {screen === 'onboarding-name' && ( { setOnboardingName(name); setScreen('onboarding-question'); }} onBack={() => setScreen('welcome')} /> )} {screen === 'onboarding-question' && ( setScreen('onboarding-name')} /> )} {screen === 'code-reveal' && ( { // Session already set, just clear screens setScreen('welcome'); }} /> )}
); }; export default LoginGate;