import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Group } from '@/components/ui/group'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { joinServerByInvite, loadFederatedServers, switchServer } from '@/features/app/actions'; import { connect, getHandshakeHash } from '@/features/server/actions'; import { initE2EE } from '@/lib/e2ee'; import { useInfo } from '@/features/server/hooks'; import { getFileUrl, getUrlFromServer } from '@/helpers/get-file-url'; import { getLocalStorageItem, LocalStorageKey, removeLocalStorageItem, setLocalStorageItem } from '@/helpers/storage'; import { useForm } from '@/hooks/use-form'; import { getAccessToken, initSupabase, supabase } from '@/lib/supabase'; import type { Provider } from '@supabase/supabase-js'; import { memo, useCallback, useMemo, useState } from 'react'; import { toast } from 'sonner'; const OAUTH_PROVIDER_LABELS: Record = { google: 'Google', discord: 'Discord', facebook: 'Facebook', twitch: 'Twitch' }; const Connect = memo(() => { const [activeTab, setActiveTab] = useState<'login' | 'register'>('login'); const [loading, setLoading] = useState(false); const info = useInfo(); const loginForm = useForm<{ email: string; password: string; rememberCredentials: boolean; }>({ email: getLocalStorageItem(LocalStorageKey.EMAIL) || '', password: '', rememberCredentials: !!getLocalStorageItem( LocalStorageKey.REMEMBER_CREDENTIALS ) }); const registerForm = useForm<{ displayName: string; email: string; password: string; }>({ displayName: '', email: '', password: '' }); const inviteCode = useMemo(() => { const urlParams = new URLSearchParams(window.location.search); const invite = urlParams.get('invite'); return invite || undefined; }, []); const ensureSupabaseReady = useCallback(() => { if (supabase) return true; if (info?.supabaseUrl && info?.supabaseAnonKey) { initSupabase(info.supabaseUrl, info.supabaseAnonKey); return !!supabase; } return false; }, [info?.supabaseUrl, info?.supabaseAnonKey]); const establishSession = useCallback( async (accessToken: string, refreshToken: string) => { const { error } = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken }); if (error) { throw new Error(`Failed to store session: ${error.message}`); } // Ensure token is visible to the ws connectionParams path before connect(). for (let i = 0; i < 5; i += 1) { const token = await getAccessToken(); if (token) return; await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error('Failed to read authentication token after login'); }, [] ); const onRememberCredentialsChange = useCallback( (checked: boolean) => { loginForm.onChange('rememberCredentials', checked); if (checked) { setLocalStorageItem(LocalStorageKey.REMEMBER_CREDENTIALS, 'true'); } else { removeLocalStorageItem(LocalStorageKey.REMEMBER_CREDENTIALS); } }, // loginForm.onChange is a stable sub-property // eslint-disable-next-line react-hooks/exhaustive-deps [loginForm.onChange] ); const onLoginClick = useCallback(async () => { setLoading(true); try { if (!ensureSupabaseReady()) { throw new Error('Authentication client is not initialized'); } const url = getUrlFromServer(); const response = await fetch(`${url}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: loginForm.values.email, password: loginForm.values.password }) }); if (!response.ok) { const data = await response.json(); loginForm.setErrors(data.errors || {}); return; } const data = (await response.json()) as { accessToken: string; refreshToken: string; }; await establishSession(data.accessToken, data.refreshToken); if (loginForm.values.rememberCredentials) { setLocalStorageItem(LocalStorageKey.EMAIL, loginForm.values.email); } await connect(); // Initialize E2EE and load federated servers (mirrors loadApp() flow) initE2EE().catch((err) => console.error('E2EE initialization failed:', err)); await loadFederatedServers(); // If there's an invite code, join that server and switch to it if (inviteCode) { try { const server = await joinServerByInvite(inviteCode); const hash = getHandshakeHash(); if (server && hash) { await switchServer(server.id, hash); } } catch { // Invite join failed — user is still connected to default server } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); toast.error(`Could not connect: ${errorMessage}`); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [loginForm.values, loginForm.setErrors, inviteCode, ensureSupabaseReady, establishSession]); const onRegisterClick = useCallback(async () => { setLoading(true); try { if (!ensureSupabaseReady()) { throw new Error('Authentication client is not initialized'); } const url = getUrlFromServer(); const response = await fetch(`${url}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: registerForm.values.email, password: registerForm.values.password, displayName: registerForm.values.displayName, invite: inviteCode }) }); if (!response.ok) { const data = await response.json(); registerForm.setErrors(data.errors || {}); return; } const data = (await response.json()) as { accessToken: string; refreshToken: string; }; await establishSession(data.accessToken, data.refreshToken); await connect(); // Initialize E2EE and load federated servers (mirrors loadApp() flow) initE2EE().catch((err) => console.error('E2EE initialization failed:', err)); await loadFederatedServers(); // If there's an invite code, join that server and switch to it if (inviteCode) { try { const server = await joinServerByInvite(inviteCode); const hash = getHandshakeHash(); if (server && hash) { await switchServer(server.id, hash); } } catch { // Invite join failed — user is still connected to default server } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); toast.error(`Could not create account: ${errorMessage}`); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [registerForm.values, registerForm.setErrors, inviteCode, ensureSupabaseReady, establishSession]); const onOAuthClick = useCallback( async (provider: string) => { if (!ensureSupabaseReady()) { toast.error('Authentication client is not initialized'); return; } const redirectTo = new URL(window.location.origin); if (inviteCode) { redirectTo.searchParams.set('invite', inviteCode); } await supabase.auth.signInWithOAuth({ provider: provider as Provider, options: { redirectTo: redirectTo.toString() } }); }, [inviteCode, ensureSupabaseReady] ); const logoSrc = useMemo(() => { if (info?.logo) { return getFileUrl(info.logo); } return '/logo.png'; }, [info]); const OAuthSection = useMemo(() => { const providers = info?.enabledAuthProviders ?? []; return providers.length > 0 ? ( <>
or
{providers.map((provider) => ( ))}
) : null; }, [info?.enabledAuthProviders, loading, onOAuthClick] ); return ( <> {/* Keyframe animation for gradient */}
{/* Insecure connection banner */} {!window.isSecureContext && (
Insecure Connection You are on an insecure connection (HTTP). Voice and video features may not work. Set up HTTPS for full functionality.
)} {/* Left Panel — Branding Hero (hidden on mobile) */}
{/* Floating decorative orbs */}
{/* Branding content */}
Pulse Chat

Pulse Chat

Pulse keeps things simple — fast voice, structured text, and a focus on stability.

No distractions. No bloat. Just connection.

{/* Right Panel — Auth Form */}
{/* Mobile gradient accent bar */}
{/* Mobile logo (shown only on small screens) */}
Pulse Chat

Pulse Chat

{/* Auth card */}
setActiveTab(v as 'login' | 'register')} > Sign In Create Account {/* Login Tab */}
Remember me
{OAuthSection}
{/* Register Tab */}
{OAuthSection} {(info?.registrationDisabled || !info?.allowNewUsers) && !inviteCode && (

Registration requires an invite link. Ask an existing member to invite you.

)} {inviteCode && ( You were invited Invite code: {inviteCode} )}
{/* Footer */}
v{VITE_APP_VERSION}
); }); export { Connect };