'use client'; import { useCallback, useEffect, useState } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { getBrowserSupabase } from '@/lib/supabase'; // Mounts at the root layout. Checks Supabase's AAL (Authenticator Assurance // Level) after every auth state change. If the user has MFA enrolled but // their session is still at aal1, we block the UI with a code challenge // until they reach aal2. Until verified, they can't see paid features. type ChallengeState = | { status: 'idle' } | { status: 'needed'; factorId: string } | { status: 'verifying'; factorId: string; challengeId: string }; export default function MFAChallenge() { const { user, session } = useAuth(); const [state, setState] = useState({ status: 'idle' }); const [code, setCode] = useState(''); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); const evaluate = useCallback(async () => { const supabase = getBrowserSupabase(); if (!supabase || !user) { setState({ status: 'idle' }); return; } const aal = await supabase.auth.mfa.getAuthenticatorAssuranceLevel(); if (aal.error) return; const { currentLevel, nextLevel } = aal.data; if (currentLevel === 'aal1' && nextLevel === 'aal2') { const { data } = await supabase.auth.mfa.listFactors(); const factor = (data?.totp ?? []).find((f) => f.status === 'verified'); if (factor) { setState({ status: 'needed', factorId: factor.id }); return; } } setState({ status: 'idle' }); }, [user]); useEffect(() => { void evaluate(); }, [evaluate, session?.access_token]); const issueChallenge = async () => { if (state.status !== 'needed') return; const supabase = getBrowserSupabase(); if (!supabase) return; const { data, error: cErr } = await supabase.auth.mfa.challenge({ factorId: state.factorId }); if (cErr || !data) { setError(cErr?.message ?? 'Could not start MFA challenge.'); return; } setState({ status: 'verifying', factorId: state.factorId, challengeId: data.id }); }; const submit = async () => { if (state.status !== 'verifying') return; const supabase = getBrowserSupabase(); if (!supabase) return; setSubmitting(true); setError(null); try { const res = await supabase.auth.mfa.verify({ factorId: state.factorId, challengeId: state.challengeId, code, }); if (res.error) { setError(res.error.message); return; } setCode(''); setState({ status: 'idle' }); } finally { setSubmitting(false); } }; if (state.status === 'idle') return null; return (

Two-factor required

Enter the 6-digit code from your authenticator app.

setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} className="mt-4 w-full rounded border px-3 py-2 text-center text-lg tracking-widest" style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border-light)', color: 'var(--text-primary)', }} placeholder="123456" /> {error && (

{error}

)}
{state.status === 'needed' ? ( ) : ( )}
); }