eeff292
import { useEffect, useRef, useState } from 'react'
import { AuthError } from '@/shared/api'
import { t } from '@/shared/i18n'
export type Notice = { kind: 'error' | 'info' | 'success'; text: string }
export type Errors = Record<string, string>
const fieldCodes: Record<string, string> = {
invalid_email: 'email',
email_taken: 'email',
invalid_username: 'username',
username_taken: 'username',
invalid_password: 'password',
invalid_birth_date: 'birth',
under_minimum_age: 'birth',
invalid_email_code: 'code',
email_code_attempts_exceeded: 'code',
totp_code_invalid: 'code',
totp_step_reused: 'code',
recovery_code_invalid: 'code',
}
export function useTask() {
const [busy, setBusy] = useState(false)
const [notice, setNotice] = useState<Notice>()
const [errors, setErrors] = useState<Errors>({})
const [code, setCode] = useState('')
const [retryUntil, setRetryUntil] = useState(0)
const active = useRef<AbortController | null>(null)
useEffect(() => () => active.current?.abort(), [])
function fail(next: Errors) {
setErrors(next)
setCode('')
setNotice({ kind: 'error', text: Object.values(next)[0] })
requestAnimationFrame(() => document.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus())
return false
}
async function run(work: (signal: AbortSignal) => Promise<void>) {
if (active.current || Date.now() < retryUntil) return
const controller = new AbortController()
active.current = controller
setBusy(true)
setNotice(undefined)
setErrors({})
setCode('')
try {
await work(controller.signal)
} catch (error) {
if (controller.signal.aborted) return
const text = error instanceof Error ? error.message : t('error.unexpected')
setNotice({ kind: 'error', text })
if (error instanceof AuthError) {
setCode(error.code)
if (error.retryAfter) setRetryUntil(Date.now() + error.retryAfter * 1000)
const field = fieldCodes[error.code]
if (field) {
setErrors({ [field]: text })
requestAnimationFrame(() => document.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus())
}
}
} finally {
if (active.current === controller) {
active.current = null
if (!controller.signal.aborted) setBusy(false)
}
}
}
return { busy, notice, setNotice, errors, code, fail, run, retryUntil }
}