'use client' import * as React from 'react' import { AlertTriangle, ChevronDown, ChevronUp } from 'lucide-react' import { useT } from '@open-mercato/shared/lib/i18n/context' import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall' import { Button } from '@open-mercato/ui/primitives/button' import { useProviderChallengeComponent } from './mfa-ui-registry' export type MfaChallengeMethod = { type: string label: string icon: string components?: { list?: string details?: string challenge?: string } } const RECOVERY_CODE_METHOD_TYPE = 'recovery_code' type MfaChallengePanelProps = { challengeId: string availableMethods: MfaChallengeMethod[] onBack: () => void } type PrepareResponse = { ok: boolean clientData?: Record } type VerifyResponse = { ok: boolean redirect?: string } function readErrorMessage(error: unknown): string { if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') { return error.message } return 'Failed to verify MFA challenge.' } export default function MfaChallengePanel({ challengeId, availableMethods, onBack, }: MfaChallengePanelProps) { const t = useT() const methods = React.useMemo(() => { const includesRecovery = availableMethods.some((method) => method.type === RECOVERY_CODE_METHOD_TYPE) if (includesRecovery) return availableMethods return [ ...availableMethods, { type: RECOVERY_CODE_METHOD_TYPE, label: t('security.login.mfaChallenge.recovery.option', '2FA recovery code'), icon: 'KeyRound', }, ] }, [availableMethods, t]) const [selectedMethod, setSelectedMethod] = React.useState(methods[0]?.type ?? '') const [loading, setLoading] = React.useState(false) const [error, setError] = React.useState(null) const [preparedMethods, setPreparedMethods] = React.useState>({}) const [showMoreOptions, setShowMoreOptions] = React.useState(false) React.useEffect(() => { if (methods.some((method) => method.type === selectedMethod)) return setSelectedMethod(methods[0]?.type ?? '') }, [methods, selectedMethod]) const verify = React.useCallback(async (methodType: string, payload: Record) => { const result = await readApiResultOrThrow('/api/security/mfa/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ challengeId, methodType, payload, }), }) const redirect = typeof result.redirect === 'string' && result.redirect.length > 0 ? result.redirect : '/backend' window.location.href = redirect }, [challengeId]) const prepare = React.useCallback(async (methodType: string) => { return readApiResultOrThrow('/api/security/mfa/prepare', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ challengeId, methodType, }), }) }, [challengeId]) const verifyRecoveryCode = React.useCallback(async (payload: Record) => { const code = typeof payload.code === 'string' ? payload.code.trim() : '' const result = await readApiResultOrThrow('/api/security/mfa/recovery', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code }), }) const redirect = typeof result.redirect === 'string' && result.redirect.length > 0 ? result.redirect : '/backend' window.location.href = redirect }, []) const handleVerify = React.useCallback(async (payload: Record) => { if (!selectedMethod) return setLoading(true) setError(null) try { if (selectedMethod === RECOVERY_CODE_METHOD_TYPE) { await verifyRecoveryCode(payload) } else { await verify(selectedMethod, payload) } } catch (err) { setError(readErrorMessage(err)) } finally { setLoading(false) } }, [selectedMethod, verify, verifyRecoveryCode]) const handlePrepare = React.useCallback(async () => { if (!selectedMethod) return setLoading(true) setError(null) try { const prepared = await prepare(selectedMethod) return prepared.clientData } catch (err) { setError(readErrorMessage(err)) } finally { setLoading(false) } }, [prepare, selectedMethod]) const handleResend = handlePrepare React.useEffect(() => { if (!selectedMethod || selectedMethod !== 'otp_email' || preparedMethods[selectedMethod]) { return } let active = true setLoading(true) setError(null) prepare(selectedMethod) .then(() => { if (!active) return setPreparedMethods((current) => ({ ...current, [selectedMethod]: true })) }) .catch((err) => { if (!active) return setError(readErrorMessage(err)) }) .finally(() => { if (!active) return setLoading(false) }) return () => { active = false } }, [prepare, preparedMethods, selectedMethod]) const selected = methods.find((method) => method.type === selectedMethod) ?? null const alternativeMethods = methods.filter((method) => method.type !== selectedMethod) const ChallengeComponent = useProviderChallengeComponent(selected ?? { type: 'unknown' }) return (
{methods.length > 0 ? (
{selected ? ( ) : null} {alternativeMethods.length > 0 ? (
{showMoreOptions ? (
{alternativeMethods.map((method) => ( ))}
) : null}
) : null} {error ? (

{error}

) : null}
) : (
)}
) }