import { useEffect, useState } from 'react'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import ProviderIcon from '../../components/ProviderIcon.jsx'
import { useAuth } from '../../contexts/AuthContext.jsx'
import { api } from '../../api/client.js'
import PlayerShell, { honeypotStyle } from './PlayerShell.jsx'
// Friendly copy for the ?sso_error codes the SSO callback can bounce back with.
const SSO_ERRORS = {
not_linked:
'That account is not linked to a player. Enable SSO sign-up, or sign in with a password and link it under your account.',
disabled: 'This account is not active. Contact an administrator.',
denied: 'Sign-in was cancelled.',
unavailable: 'That sign-in method is not available right now.',
bad_state: 'Your sign-in session expired. Please try again.',
error: 'Could not complete sign-in. Please try again.',
}
export default function PlayerLogin() {
const { user, login, loginTotp, ssoLoginTotp } = useAuth()
const navigate = useNavigate()
const location = useLocation()
const dest = location.state?.from?.pathname || '/account'
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [company, setCompany] = useState('') // honeypot — must stay empty
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const [stage, setStage] = useState('creds') // 'creds' | 'totp'
const [challenge, setChallenge] = useState('')
const [code, setCode] = useState('')
const [ssoTotp, setSsoTotp] = useState(false)
const [providers, setProviders] = useState([])
const [canRegister, setCanRegister] = useState(false)
const ssoError = SSO_ERRORS[new URLSearchParams(location.search).get('sso_error')] || ''
// A signed-in player goes straight to their account.
useEffect(() => {
if (user && user.role === 'player') navigate(dest, { replace: true })
}, [user, dest, navigate])
// The SSO callback bounces 2FA accounts back here with ?sso_totp=1.
useEffect(() => {
if (new URLSearchParams(location.search).get('sso_totp')) {
setStage('totp')
setSsoTotp(true)
}
}, [location.search])
// SSO providers (for buttons) + whether password registration is open.
useEffect(() => {
let active = true
api
.authProviders()
.then((list) => active && setProviders(Array.isArray(list) ? list : []))
.catch(() => active && setProviders([]))
api
.publicSettings()
.then((s) => active && setCanRegister(Boolean(s?.registration?.password)))
.catch(() => {})
return () => {
active = false
}
}, [])
function startSso(provider) {
// Always return into the player portal so the callback lands on /account*.
const q = `?returnTo=${encodeURIComponent(dest.startsWith('/account') ? dest : '/account')}`
window.location.assign(provider.loginUrl + q)
}
async function onSubmit(e) {
e.preventDefault()
setError('')
setBusy(true)
try {
const data = await login(username, password, { company })
if (data.totpRequired) {
setChallenge(data.challenge)
setStage('totp')
setBusy(false)
return
}
navigate(dest, { replace: true })
} catch (err) {
if (err.status === 403) setError('This account is not active. Contact an administrator.')
else setError(err.status === 401 ? 'Incorrect username or password.' : 'Could not sign in right now.')
setBusy(false)
}
}
async function onSubmitTotp(e) {
e.preventDefault()
setError('')
setBusy(true)
try {
if (ssoTotp) {
const { returnTo } = await ssoLoginTotp(code)
navigate(returnTo || '/account', { replace: true })
} else {
await loginTotp(challenge, code)
navigate(dest, { replace: true })
}
} catch (err) {
const expired = err.status === 401 && /expired/i.test(err.message)
setError(expired ? 'Your verification session expired. Please sign in again.' : 'Invalid verification code.')
setBusy(false)
if (expired) {
setStage('creds')
setSsoTotp(false)
}
}
}
return (