import { useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import ShardAccountActions from './ShardAccountActions.jsx'
import CreateGameAccountForm from './CreateGameAccountForm.jsx'
import api from '../api.js'
import { useGameAccountSignup } from '../lib/useShardFeatures.js'
import { ErrorState, Loading } from '../core.js'
// Shared game-account linking + character roster, used by both the player portal
// (/player) and the staff account page (/admin/account). `scope` is the api
// object with { link, accounts, roster } (player or admin self-service); `charTo`
// maps a serial to the route for that character's sheet. `readOnly` drops the
// link forms and self-voice copy for the admin case where staff view *another*
// user's accounts (no `scope.link`) at /admin/users/:id.
function LinkForm({ scope, onLinked, compact }) {
const [code, setCode] = useState('')
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState('')
const [error, setError] = useState('')
async function submit(e) {
e.preventDefault()
setMsg(''); setError('')
if (!code.trim()) return
setBusy(true)
try {
const { account } = await scope.link(code.trim())
setMsg(`Linked ${account}.`)
setCode('')
await onLinked()
} catch (err) {
setError(err.message || 'Could not link that code.')
} finally {
setBusy(false)
}
}
return (
)
}
// Compact per-account "Unlink" button for the admin (readOnly) view. Confirms,
// then calls onUnlink(account) and reloads. Errors surface inline.
function UnlinkButton({ account, onUnlink }) {
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
async function go() {
if (!window.confirm(`Unlink game account “${account}” from this user? Attribution stops immediately.`)) return
setBusy(true); setError('')
try {
await onUnlink(account)
} catch (err) {
const byStatus = { 403: 'Protected account — refused.', 404: 'Not linked.' }
setError(byStatus[err.status] || err.message || 'Could not unlink.')
setBusy(false)
}
}
return (
{error && {error}}
)
}
export default function GameAccounts({ scope, charTo, readOnly = false, moderation = false, onUnlink = null }) {
const [accounts, setAccounts] = useState(null)
const [error, setError] = useState('')
// Whether the site currently offers game-account creation. Only relevant for
// the self-service (non-readOnly) view with a createAccount scope.
//
// From OUR public features endpoint as of slice 3, not core's public settings:
// the flag derives from the `uo.game_account_signup` setting, which this module
// owns, because "the game server's own SignupMode must agree" is not a sentence
// core can own. Same cached call the nav gates use, so this costs no round-trip.
const signupOk = useGameAccountSignup()
const load = useCallback(async () => {
setError('')
try {
setAccounts(await scope.accounts())
} catch {
setError(readOnly ? 'Could not load this user’s game accounts.' : 'Could not load your game accounts.')
}
}, [scope, readOnly])
useEffect(() => { load() }, [load])
const canCreate = !readOnly && Boolean(scope.createAccount) && signupOk === true
if (error) return
if (!accounts) return
// No linked accounts. In read-only (admin viewing another user) this is just an
// empty state; otherwise it's the link-your-account prompt.
if (accounts.length === 0) {
if (readOnly) {
return (
This user has not linked a game account.
)
}
return (
Link your game account
Already play? In game, type [link to get a
one-time code, then enter it below to see your characters, stats, skills and vendors here.