import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import type { AiturkCliStatus } from '@/global' import { useAiturkCopy } from '@/i18n/aiturk' import { aiturkLoginScope } from '@/lib/aiturk-provider' import { Loader2 } from '@/lib/icons' import { notify } from '@/store/notifications' const POLL_INTERVAL_MS = 3000 type Phase = 'idle' | 'starting' | 'waiting' | 'question' | 'replacing' interface Waiting { userCode: string verificationUri: string } type LoginCopy = ReturnType function errorCode(error: unknown): string { // The main process only ever throws fixed codes ("aiturk-login:"). return /aiturk-login:([a-z-]+)/.exec(error instanceof Error ? error.message : String(error))?.[1] ?? '' } function errorText(code: string, copy: LoginCopy): string { switch (code) { case 'rate-limited': return copy.login.rateLimited case 'browser-failed': return copy.login.browserFailed case 'no-models': return copy.noModels case 'provider-failed': case 'invalid-response': return copy.failed default: return copy.login.unavailable } } function cliText(cli: AiturkCliStatus, copy: LoginCopy): string { switch (cli) { case 'ok': return copy.login.cliOk case 'outdated': return copy.login.cliOutdated case 'failed': case 'unknown-account': return copy.login.cliFailed default: // 'missing' stays silent; 'other-account' is asked as a question. return '' } } /** * One-click TurkServis sign-in, shared by onboarding and Settings. * * The API keys never reach this component: the main process shows us only the * short code, then reports `connected` once it has saved the provider and * handed the AITURK CLI its own key. */ export function useAiturkLogin(onConnected: () => Promise | void) { const copy = useAiturkCopy() const [phase, setPhase] = useState('idle') const [waiting, setWaiting] = useState(null) const [error, setError] = useState('') const [info, setInfo] = useState('') const live = useRef(false) const mounted = useRef(true) const onConnectedRef = useRef(onConnected) const copyRef = useRef(copy) onConnectedRef.current = onConnected copyRef.current = copy // eslint-disable-next-line no-restricted-syntax -- mount flag + in-flight login flag, not an atom mirror useEffect(() => { mounted.current = true return () => { mounted.current = false // Leaving mid-login: revoke the code and drop anything main still holds. if (live.current) { live.current = false void window.hermesDesktop?.aiturkLogin?.cancel().catch(() => {}) } } }, []) const finish = useCallback(async (cli: AiturkCliStatus) => { live.current = false const text = cliText(cli, copyRef.current) if (mounted.current) { setWaiting(null) setPhase('idle') setInfo(text) } try { await onConnectedRef.current() } catch { // The provider is saved; a failed refresh is the caller's concern. } // Onboarding closes this panel as soon as it is connected; keep the CLI // note visible as a toast in that case. if (!mounted.current && text) { notify({ kind: cli === 'ok' ? 'success' : 'info', message: text }) } }, []) // eslint-disable-next-line no-restricted-syntax -- clears the in-flight login flag when polling ends useEffect(() => { if (phase !== 'waiting') { return } let stopped = false let timer: ReturnType | undefined const tick = async () => { const bridge = window.hermesDesktop?.aiturkLogin if (stopped || !bridge) { return } let result try { result = await bridge.poll() } catch { result = { state: 'pending' as const } } if (stopped) { return } if (result.state === 'pending') { timer = setTimeout(() => void tick(), POLL_INTERVAL_MS) return } if (result.state === 'connected') { if (result.cli === 'other-account') { setWaiting(null) setPhase('question') return } await finish(result.cli) return } live.current = false setWaiting(null) setPhase('idle') setError(result.state === 'expired' ? copyRef.current.login.expired : errorText(result.error, copyRef.current)) } timer = setTimeout(() => void tick(), POLL_INTERVAL_MS) return () => { stopped = true clearTimeout(timer) } }, [phase, finish]) const start = useCallback(async () => { const bridge = window.hermesDesktop?.aiturkLogin setError('') setInfo('') if (!bridge) { setError(copyRef.current.login.unsupported) return } setPhase('starting') try { const started = await bridge.start(aiturkLoginScope()) if (!mounted.current) { void bridge.cancel().catch(() => {}) return } live.current = true setWaiting({ userCode: started.userCode, verificationUri: started.verificationUri }) setPhase('waiting') } catch (err) { if (mounted.current) { setPhase('idle') setError(errorText(errorCode(err), copyRef.current)) } } }, []) const cancel = useCallback(() => { live.current = false setWaiting(null) setPhase('idle') void window.hermesDesktop?.aiturkLogin?.cancel().catch(() => {}) }, []) const reopen = useCallback(() => { if (waiting) { void window.hermesDesktop?.openExternal(waiting.verificationUri).catch(() => {}) } }, [waiting]) const replaceCli = useCallback(async () => { setPhase('replacing') let cli: AiturkCliStatus = 'failed' try { cli = (await window.hermesDesktop?.aiturkLogin?.overwriteCli())?.cli ?? 'failed' } catch { cli = 'failed' } await finish(cli === 'other-account' ? 'failed' : cli) }, [finish]) const keepCli = useCallback(async () => { void window.hermesDesktop?.aiturkLogin?.cancel().catch(() => {}) await finish('missing') }, [finish]) return { phase, waiting, error, info, start, cancel, reopen, replaceCli, keepCli } } interface AiturkLoginProps { /** Runs once the TurkServis provider is saved (refresh lists, advance onboarding…). */ onConnected: () => Promise | void /** Idle button label; defaults to "Giriş yap". */ label?: string variant?: 'default' | 'outline' disabled?: boolean } export function AiturkLogin({ onConnected, label, variant = 'default', disabled }: AiturkLoginProps) { const copy = useAiturkCopy() const login = useAiturkLogin(onConnected) if (login.phase === 'waiting' && login.waiting) { return (
{copy.login.codeLabel}
{login.waiting.userCode}

{copy.login.confirmHint}

) } if (login.phase === 'question' || login.phase === 'replacing') { const replacing = login.phase === 'replacing' return (

{copy.login.cliOtherAccount}

) } const starting = login.phase === 'starting' return (
{login.error ? (

{login.error}

) : null} {login.info ? (

{login.info}

) : null}
) }