import { useEffect, useState, useCallback, Suspense, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { Loader2, AlertCircle, CheckCircle2, AlertTriangle, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; import langbotIcon from '@/app/assets/langbot-logo.webp'; type SpaceOAuthLoginResult = { token: string; user: string; }; const pendingSpaceOAuthLogins = new Map< string, Promise >(); function getOrCreateSpaceOAuthLoginPromise( authCode: string, ): Promise { const pendingRequest = pendingSpaceOAuthLogins.get(authCode); if (pendingRequest) { return pendingRequest; } const requestPromise = httpClient .exchangeSpaceOAuthCode(authCode) .finally(() => { pendingSpaceOAuthLogins.delete(authCode); }); pendingSpaceOAuthLogins.set(authCode, requestPromise); return requestPromise; } function SpaceOAuthCallbackContent() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { t } = useTranslation(); const isMountedRef = useRef(true); const [status, setStatus] = useState< 'loading' | 'confirm' | 'success' | 'error' >('loading'); const [errorMessage, setErrorMessage] = useState(''); const [isBindMode, setIsBindMode] = useState(false); const [code, setCode] = useState(null); const [isProcessing, setIsProcessing] = useState(false); const [localEmail, setLocalEmail] = useState(''); const handleOAuthCallback = useCallback( async (authCode: string) => { try { const response = await getOrCreateSpaceOAuthLoginPromise(authCode); if (!isMountedRef.current) { return; } localStorage.setItem('token', response.token); if (response.user) { localStorage.setItem('userEmail', response.user); } setStatus('success'); toast.success(t('common.spaceLoginSuccess')); // If wizard state exists, redirect back to wizard instead of home const wizardState = localStorage.getItem('langbot_wizard_state'); const redirectTo = wizardState ? '/wizard' : '/home'; setTimeout(() => { navigate(redirectTo); }, 1000); } catch (err) { if (!isMountedRef.current) { return; } setStatus('error'); const errorObj = err as { msg?: string }; const errMsg = (errorObj?.msg || '').toLowerCase(); if (errMsg.includes('account email mismatch')) { setErrorMessage(t('account.spaceEmailMismatch')); } else { setErrorMessage(t('common.spaceLoginFailed')); } } }, [navigate, t], ); const [bindState, setBindState] = useState(null); const handleBindAccount = useCallback( async (authCode: string, state: string) => { setIsProcessing(true); try { const response = await httpClient.bindSpaceAccount(authCode, state); if (!isMountedRef.current) { return; } localStorage.setItem('token', response.token); if (response.user) { localStorage.setItem('userEmail', response.user); } setStatus('success'); toast.success(t('account.bindSpaceSuccess')); setTimeout(() => { navigate('/home'); }, 1000); } catch (err) { if (!isMountedRef.current) { return; } setStatus('error'); const errorObj = err as { msg?: string }; const errMsg = (errorObj?.msg || '').toLowerCase(); if (errMsg.includes('account email mismatch')) { setErrorMessage(t('account.spaceEmailMismatch')); } else { setErrorMessage(t('account.bindSpaceFailed')); } } finally { if (isMountedRef.current) { setIsProcessing(false); } } }, [navigate, t], ); useEffect(() => { isMountedRef.current = true; const authCode = searchParams.get('code'); const error = searchParams.get('error'); const errorDescription = searchParams.get('error_description'); const mode = searchParams.get('mode'); const state = searchParams.get('state'); if (error) { setStatus('error'); setErrorMessage( errorDescription || error || t('common.spaceLoginFailed'), ); return; } if (!authCode) { setStatus('error'); setErrorMessage(t('common.spaceLoginNoCode')); return; } setCode(authCode); if (mode === 'bind') { // Bind mode - verify state (token) exists if (!state) { setStatus('error'); setErrorMessage(t('account.bindSpaceInvalidState')); return; } setBindState(state); setIsBindMode(true); setLocalEmail(localStorage.getItem('userEmail') || ''); setStatus('confirm'); } else { // Normal login/register mode handleOAuthCallback(authCode); } return () => { isMountedRef.current = false; }; }, [searchParams, handleOAuthCallback, t]); const handleConfirmBind = () => { if (code && bindState) { handleBindAccount(code, bindState); } }; const handleCancelBind = () => { navigate('/home'); }; return (
LangBot {status === 'loading' && t('common.spaceLoginProcessing')} {status === 'confirm' && t('account.bindSpaceConfirmTitle')} {status === 'success' && (isBindMode ? t('account.bindSpaceSuccess') : t('common.spaceLoginSuccess'))} {status === 'error' && (isBindMode ? t('account.bindSpaceFailed') : t('common.spaceLoginError'))} {status === 'loading' && t('common.spaceLoginProcessingDescription')} {status === 'confirm' && t('account.bindSpaceConfirmDescription')} {status === 'success' && t('common.spaceLoginSuccessDescription')} {status === 'error' && errorMessage} {status === 'loading' && } {status === 'confirm' && ( <>

{t('account.bindSpaceWarning', { localEmail: localEmail || '-', })}

)} {status === 'success' && ( )} {status === 'error' && ( <> )}
); } function LoadingFallback() { return (
); } export default function SpaceOAuthCallback() { return ( }> ); }