import { useEffect, useRef, useState } from 'react' import { createRequestScope, errorMessage } from '../services/async' import { isNative, native } from '../services/native' import { linkedNickname, validCredentials } from '../state/account' import { isValidNickname } from '../state/settings' import type { ShaCraftAccount, LinkChallenge } from '../types/launcher' interface PendingLink { challengeId: number expiresAt: number isCurrent: () => boolean } export function useAccount() { // undefined = restoring saved account; null = signed out. const [account, setAccount] = useState(isNative() ? undefined : null) const [error, setError] = useState(null) const [busy, setBusy] = useState(false) const [recoveryCodes, setRecoveryCodes] = useState([]) const [challenge, setChallenge] = useState(null) const [linkMessage, setLinkMessage] = useState(null) const pending = useRef(false) const requests = useRef(createRequestScope()) useEffect(() => { if (!isNative()) return let active = true void native.getAccount().then((value) => { if (active) setAccount(value) }).catch((reason: unknown) => { if (!active) return setAccount(null) setError(errorMessage(reason, 'Не удалось восстановить аккаунт ShaCraft')) }) return () => { active = false; requests.current.invalidate() } }, []) useEffect(() => { if (!challenge) return let active = true let timer: ReturnType | undefined const current = () => active && challenge.isCurrent() const poll = async () => { if (!current()) return if (Date.now() >= challenge.expiresAt) { setChallenge(null) setLinkMessage('Срок проверки истёк. Начните привязку ещё раз.') return } try { const result = await native.linkStatus(challenge.challengeId) if (!current()) return if (result.status === 'verified') { const refreshed = await native.getAccount() if (!current()) return setAccount(refreshed) setChallenge(null) setLinkMessage(linkedNickname(refreshed) ? 'Ник подтверждён.' : 'Не удалось подтвердить привязку. Войдите снова.') } else if (result.status === 'expired' || result.status === 'conflict') { setChallenge(null) setLinkMessage(result.detail || 'Проверка завершилась. Попробуйте ещё раз.') } else { // One request at a time; dispose and logout cancel future polling. timer = setTimeout(() => { void poll() }, 3_000) } } catch (reason) { if (!current()) return setChallenge(null) setLinkMessage(errorMessage(reason, 'Не удалось проверить ник')) } } timer = setTimeout(() => { void poll() }, 3_000) return () => { active = false; clearTimeout(timer) } }, [challenge]) const authenticate = async (username: string, password: string, register: boolean) => { if (pending.current || account === undefined) return false if (!validCredentials(username, password)) { setError('Логин: 3–32 латинских буквы, цифры или _; пароль: 3–128 символов') return false } if (!isNative()) { setError('Вход в ShaCraft доступен в приложении лаунчера') return false } pending.current = true requests.current.invalidate() const currentRequest = requests.current.capture() setBusy(true) setError(null) try { const result = await native.authenticate(username, password, register) if (!currentRequest()) return false setAccount(result.account) setChallenge(null) setLinkMessage(null) setRecoveryCodes(result.recoveryCodes) return true } catch (reason) { setError(errorMessage(reason, register ? 'Не удалось зарегистрироваться' : 'Не удалось войти')) return false } finally { pending.current = false setBusy(false) } } const logout = async () => { if (!isNative() || pending.current) return pending.current = true requests.current.invalidate() setChallenge(null) setLinkMessage(null) setBusy(true) setError(null) try { await native.logout() setAccount(null) setRecoveryCodes([]) } catch (reason) { setError(errorMessage(reason, 'Не удалось выйти из ShaCraft')) } finally { pending.current = false setBusy(false) } } const acceptChallenge = (started: LinkChallenge) => { requests.current.invalidate() const current = requests.current.capture() setLinkMessage(`${started.registered_on_server ? 'Войдите на Aeronautics: /login <пароль>.' : 'Войдите на Aeronautics: /register <пароль> <пароль>.'} Затем подтвердите свой аккаунт командой /shacraft link ${started.challenge_id} ${started.proof_code}`) setChallenge({ challengeId: started.challenge_id, expiresAt: Date.now() + started.expires_in_seconds * 1000, isCurrent: current }) } const startLink = async (nickname: string) => { if (!isNative() || pending.current || challenge || !account) return if (!isValidNickname(nickname)) { setLinkMessage('Ник: 3–16 латинских букв, цифр или _') return } pending.current = true setBusy(true) setLinkMessage('Создаём проверку…') requests.current.invalidate() const currentRequest = requests.current.capture() try { const started = await native.startLink(nickname) if (!currentRequest()) return acceptChallenge(started) } catch (reason) { setLinkMessage(errorMessage(reason, 'Не удалось начать привязку')) } finally { pending.current = false setBusy(false) } } return { account, error, busy, recoveryCodes, linkMessage, linking: challenge !== null, linkedNickname: linkedNickname(account), authenticate, logout, startLink, acceptChallenge, clearError: () => setError(null), acknowledgeRecoveryCodes: () => setRecoveryCodes([]), } }